Merge branch 'develop' into 'master'

Azure deploy & Signup flow

See merge request sahkoinsinoorikilta/vtmk/web2.0-backend!32
This commit is contained in:
Aarni Halinen
2020-12-12 12:11:09 +00:00
23 changed files with 535 additions and 5375 deletions
+9 -3
View File
@@ -5,15 +5,21 @@ from webapp.utils import month_from_now
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from auditlog.registry import auditlog from auditlog.registry import auditlog
from phonenumber_field.modelfields import PhoneNumberField from phonenumber_field.modelfields import PhoneNumberField
from webapp.models import BaseRole
import logging import logging
import webapp.models
# TODO: Move BaseRole to Kaehmt App; will fuck up the DB since table is removed, if no data migration is done before-hand.
# Either reconstruct all kaehmy roles from scratch then, or do these migrations:
# 1. Create table here
# 2. Data migrate from webapp BaseRole to new kaehmy BaseRole
# 3. Delete webapp BaseRole table
VERBOSE_NAME = _('Kaehmy') VERBOSE_NAME = _('Kaehmy')
class KaehmyBaseRole(webapp.models.BaseRole): class KaehmyBaseRole(BaseRole):
"""ABC"""
CATEGORIES = ( CATEGORIES = (
('corporate', _('Corporate affairs')), ('corporate', _('Corporate affairs')),
+3
View File
@@ -19,6 +19,9 @@
Jos sinulla on kysyttävää mistä tahansa virasta, kannattaa konsultoida <a href="https://static.sika.sik.party/uus_webi/kahmyopas.pdf">kaehmyopasta</a> Jos sinulla on kysyttävää mistä tahansa virasta, kannattaa konsultoida <a href="https://static.sika.sik.party/uus_webi/kahmyopas.pdf">kaehmyopasta</a>
tai olla yhteydessä kyseistä virkaa tänä vuonna toimittavaan henkilöön.{% endblocktrans %} tai olla yhteydessä kyseistä virkaa tänä vuonna toimittavaan henkilöön.{% endblocktrans %}
</p> </p>
<p>
{% blocktrans %}(HUOM! Kaehmytekstin maksimipituus on 300 merkkiä. Tarvittaessa voit kirjoittaa lisätietoja kommenteihin.){% endblocktrans %}
</p>
<p> <p>
{% blocktrans %}Muista, että kaehmyn lähettäminen on kiinnostuksen ilmaus {% blocktrans %}Muista, että kaehmyn lähettäminen on kiinnostuksen ilmaus
eikä siis missään nimessä sitova ilmoittautumien mihinkään tehtävään!{% endblocktrans %} eikä siis missään nimessä sitova ilmoittautumien mihinkään tehtävään!{% endblocktrans %}
Binary file not shown.
File diff suppressed because it is too large Load Diff
Binary file not shown.
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -22,7 +22,7 @@ nose-exclude==0.5.0
psycopg2-binary==2.8.4 psycopg2-binary==2.8.4
django-bootstrap3==11.1.0 django-bootstrap3==11.1.0
django-tables2==1.6.1 django-tables2==1.6.1
pycodestyle==2.3.1 pycodestyle==2.6.0
dealer==2.0.5 dealer==2.0.5
django-modeltranslation==0.13b1 django-modeltranslation==0.13b1
django-auditlog==0.4.5 django-auditlog==0.4.5
+23 -23
View File
@@ -8,15 +8,15 @@ services:
condition: on-failure condition: on-failure
environment: environment:
- POSTGRES_USER_FILE=/run/secrets/DJANGO_DB_USER - POSTGRES_USER_FILE=/run/secrets/BACKEND_DB_USER
- POSTGRES_PASSWORD_FILE=/run/secrets/DJANGO_DB_PASSWD - POSTGRES_PASSWORD_FILE=/run/secrets/BACKEND_DB_PASSWD
ports: ports:
- 5432:5432 - 5432:5432
volumes: volumes:
- dbdata:/var/lib/postgresql/data - dbdata:/var/lib/postgresql/data
secrets: secrets:
- DJANGO_DB_USER - BACKEND_DB_USER
- DJANGO_DB_PASSWD - BACKEND_DB_PASSWD
backend: backend:
image: registry.gitlab.com/sahkoinsinoorikilta/vtmk/web2.0-backend:latest image: registry.gitlab.com/sahkoinsinoorikilta/vtmk/web2.0-backend:latest
@@ -42,38 +42,38 @@ services:
target: /app/collected_static target: /app/collected_static
environment: environment:
- SECRET_KEY_FILE=/run/secrets/DJANGO_SECRET_KEY - SECRET_KEY_FILE=/run/secrets/BACKEND_SECRET_KEY
- TG_BOT_TOKEN_FILE=/run/secrets/DJANGO_TG_BOT_TOKEN - TG_BOT_TOKEN_FILE=/run/secrets/BACKEND_TG_BOT_TOKEN
- DB_USER_FILE=/run/secrets/DJANGO_DB_USER - DB_USER_FILE=/run/secrets/BACKEND_DB_USER
- DB_PASSWD_FILE=/run/secrets/DJANGO_DB_PASSWD - DB_PASSWD_FILE=/run/secrets/BACKEND_DB_PASSWD
- HOST=api.sika.sik.party - HOST=api.sika.sik.party
- FRONTEND_URL=sika.sik.party - FRONTEND_URL=sika.sik.party
- DB_HOST=db - DB_HOST=db
- DB_PORT=5432 - DB_PORT=5432
- EMAIL_API_KEY_FILE=/run/secrets/DJANGO_EMAIL_API_KEY - EMAIL_API_KEY_FILE=/run/secrets/BACKEND_EMAIL_API_KEY
- EMAIL_API_SECRET_FILE=/run/secrets/DJANGO_EMAIL_API_SECRET - EMAIL_API_SECRET_FILE=/run/secrets/BACKEND_EMAIL_API_SECRET
secrets: secrets:
- DJANGO_SECRET_KEY - BACKEND_SECRET_KEY
- DJANGO_TG_BOT_TOKEN - BACKEND_TG_BOT_TOKEN
- DJANGO_DB_USER - BACKEND_DB_USER
- DJANGO_DB_PASSWD - BACKEND_DB_PASSWD
- DJANGO_EMAIL_API_KEY - BACKEND_EMAIL_API_KEY
- DJANGO_EMAIL_API_SECRET - BACKEND_EMAIL_API_SECRET
secrets: secrets:
DJANGO_SECRET_KEY: BACKEND_SECRET_KEY:
external: true external: true
DJANGO_TG_BOT_TOKEN: BACKEND_TG_BOT_TOKEN:
external: true external: true
DJANGO_DB_NAME: BACKEND_DB_NAME:
external: true external: true
DJANGO_DB_USER: BACKEND_DB_USER:
external: true external: true
DJANGO_DB_PASSWD: BACKEND_DB_PASSWD:
external: true external: true
DJANGO_EMAIL_API_KEY: BACKEND_EMAIL_API_KEY:
external: true external: true
DJANGO_EMAIL_API_SECRET: BACKEND_EMAIL_API_SECRET:
external: true external: true
volumes: volumes:
+2 -6
View File
@@ -1,8 +1,7 @@
"""File containing webapp app admin registers.""" """File containing webapp app admin registers."""
from django.contrib import admin from django.contrib import admin
from webapp.models import Official, Role, Committee, Occupation from webapp.models import Feed, Tag, Event, Signup, SignupForm, TemplateQuestion, JobAd
from webapp.models import Feed, Tag, Event, Signup, SignupForm, TemplateQuestion
from modeltranslation.admin import TranslationAdmin from modeltranslation.admin import TranslationAdmin
from django.contrib.auth.models import Permission from django.contrib.auth.models import Permission
# this is needed so that the models get registered for translation # this is needed so that the models get registered for translation
@@ -16,7 +15,4 @@ admin.site.register(Event, TranslationAdmin)
admin.site.register(SignupForm, TranslationAdmin) admin.site.register(SignupForm, TranslationAdmin)
admin.site.register(Signup, TranslationAdmin) admin.site.register(Signup, TranslationAdmin)
admin.site.register(TemplateQuestion, TranslationAdmin) admin.site.register(TemplateQuestion, TranslationAdmin)
admin.site.register(Committee, TranslationAdmin) admin.site.register(JobAd, TranslationAdmin)
admin.site.register(Official)
admin.site.register(Occupation)
admin.site.register(Role)
+38
View File
@@ -0,0 +1,38 @@
# Generated by Django 2.1.5 on 2020-11-03 15:38
from django.db import migrations, models
import django.utils.timezone
import webapp.utils
class Migration(migrations.Migration):
dependencies = [
('webapp', '0071_auto_20201006_1749'),
]
operations = [
migrations.CreateModel(
name='JobAd',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('title', models.CharField(max_length=255)),
('title_fi', models.CharField(max_length=255, null=True)),
('title_en', models.CharField(max_length=255, null=True)),
('description', models.CharField(max_length=255)),
('description_fi', models.CharField(max_length=255, null=True)),
('description_en', models.CharField(max_length=255, null=True)),
('content', models.TextField()),
('content_fi', models.TextField(null=True)),
('content_en', models.TextField(null=True)),
('visible', models.BooleanField(default=True)),
('created_at', models.DateTimeField(default=django.utils.timezone.now)),
('autohide_at', models.DateTimeField(default=webapp.utils.month_from_now)),
('autohide_enabled', models.BooleanField(default=False)),
],
options={
'verbose_name': 'JobAd',
'verbose_name_plural': 'JobAds',
},
),
]
@@ -0,0 +1,60 @@
# Generated by Django 2.1.5 on 2020-11-07 17:16
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('webapp', '0072_jobad'),
]
operations = [
migrations.RemoveField(
model_name='occupation',
name='role',
),
migrations.RemoveField(
model_name='official',
name='role_history',
),
migrations.RemoveField(
model_name='official',
name='user',
),
migrations.RemoveField(
model_name='presetrole',
name='baserole_ptr',
),
migrations.RemoveField(
model_name='role',
name='committee',
),
migrations.RemoveField(
model_name='role',
name='presetrole_ptr',
),
migrations.RemoveField(
model_name='baserole',
name='name_en',
),
migrations.RemoveField(
model_name='baserole',
name='name_fi',
),
migrations.DeleteModel(
name='Committee',
),
migrations.DeleteModel(
name='Occupation',
),
migrations.DeleteModel(
name='Official',
),
migrations.DeleteModel(
name='PresetRole',
),
migrations.DeleteModel(
name='Role',
),
]
+18
View File
@@ -0,0 +1,18 @@
# Generated by Django 2.1.5 on 2020-11-07 18:00
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('webapp', '0073_auto_20201107_1916'),
]
operations = [
migrations.AddField(
model_name='signup',
name='deleted',
field=models.BooleanField(default=False),
),
]
+49 -135
View File
@@ -22,14 +22,14 @@ EMAIL_REGEX = r"(^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$)"
class Tag(models.Model): class Tag(models.Model):
"""Model for tag.""" """Model for tag."""
slug = models.SlugField(unique=True)
name = models.CharField(max_length=127)
icon = models.ImageField()
class Meta: class Meta:
verbose_name = _('Tag') verbose_name = _('Tag')
verbose_name_plural = _('Tags') verbose_name_plural = _('Tags')
slug = models.SlugField(unique=True)
name = models.CharField(max_length=127)
icon = models.ImageField()
def __str__(self): def __str__(self):
return _('Tag: {}').format(self.slug) return _('Tag: {}').format(self.slug)
@@ -48,6 +48,10 @@ class BaseFeed(models.Model):
class Feed(BaseFeed): class Feed(BaseFeed):
"""Model representing feed.""" """Model representing feed."""
class Meta:
verbose_name = _('Feed')
verbose_name_plural = _('Feeds')
publish_time = models.DateTimeField(default=timezone.now) publish_time = models.DateTimeField(default=timezone.now)
autohide = models.DateTimeField(default=month_from_now) autohide = models.DateTimeField(default=month_from_now)
autohide_enabled = models.BooleanField(default=False) autohide_enabled = models.BooleanField(default=False)
@@ -55,13 +59,13 @@ class Feed(BaseFeed):
def __str__(self): def __str__(self):
return _('Feed: {}').format(self.title) return _('Feed: {}').format(self.title)
class Meta:
verbose_name = _('Feed')
verbose_name_plural = _('Feeds')
class Event(BaseFeed): class Event(BaseFeed):
"""Model for event.""" """Model for event in guild calendar"""
class Meta:
verbose_name = _('Event')
verbose_name_plural = _('Events')
start_time = models.DateTimeField(default=timezone.now) start_time = models.DateTimeField(default=timezone.now)
end_time = models.DateTimeField(default=timezone.now) end_time = models.DateTimeField(default=timezone.now)
@@ -72,26 +76,30 @@ class Event(BaseFeed):
def __str__(self): def __str__(self):
return _('Event: {}').format(self.title) return _('Event: {}').format(self.title)
class Meta:
verbose_name = _('Event')
verbose_name_plural = _('Events')
class TemplateQuestion(models.Model): class TemplateQuestion(models.Model):
"""Stores template questions for signup forms as JSONB""" """
NOT IMPLEMENTED!!!
Stores template questions for signup forms as JSON format. Used in signup form creation
"""
class Meta:
verbose_name = _('Template question')
verbose_name_plural = _('Template questions')
name = models.CharField(max_length=255) name = models.CharField(max_length=255)
question = JSONField() question = JSONField()
def __str__(self): def __str__(self):
return _('Template questions: {}').format(self.name) return _('Template questions: {}').format(self.name)
class Meta:
verbose_name = _('Template question')
verbose_name_plural = _('Template questions')
class SignupForm(models.Model): class SignupForm(models.Model):
"""Model for event signup form. Stores questions in JSONB.""" """Model for event signup form. Stores questions in JSON format."""
class Meta:
verbose_name = _('Signup form')
verbose_name_plural = _('Signup forms')
title = models.CharField(max_length=255) title = models.CharField(max_length=255)
start_time = models.DateTimeField(default=timezone.now) start_time = models.DateTimeField(default=timezone.now)
@@ -107,19 +115,22 @@ class SignupForm(models.Model):
@property @property
def signups(self): def signups(self):
return Signup.objects.filter(signupForm=self).order_by('pk') return Signup.objects.filter(signupForm=self, deleted=False).order_by('pk')
@property @property
def isOpen(self): def isOpen(self):
now = timezone.now() now = timezone.now()
return self.start_time <= now and now < self.end_time return self.start_time <= now and now < self.end_time
class Meta:
verbose_name = _('Signup form')
verbose_name_plural = _('Signup forms')
class Signup(models.Model): class Signup(models.Model):
"""
Actual signup into any SignupForm. Deletes are soft.
"""
class Meta:
verbose_name = _('Sign-up')
verbose_name_plural = _('Sign-ups')
signupForm = models.ForeignKey('SignupForm', on_delete=models.CASCADE) signupForm = models.ForeignKey('SignupForm', on_delete=models.CASCADE)
time = models.DateTimeField(default=timezone.now) time = models.DateTimeField(default=timezone.now)
answer = JSONField() answer = JSONField()
@@ -129,18 +140,14 @@ class Signup(models.Model):
email = models.EmailField(blank=True, null=True) email = models.EmailField(blank=True, null=True)
# Random unique identifier. Used for signup editing by the user. # Random unique identifier. Used for signup editing by the user.
uuid = models.UUIDField(default=uuid4, editable=False) uuid = models.UUIDField(default=uuid4, editable=False)
deleted = models.BooleanField(default=False)
def __str__(self): def __str__(self):
return f"{self.signupForm}: {self.list_name} ({self.pk})" return f"{self.signupForm}: {self.list_name} ({self.pk})"
class Meta:
verbose_name = _('Sign-up')
verbose_name_plural = _('Sign-ups')
@receiver(post_save, sender=Signup) @receiver(post_save, sender=Signup)
def email_on_signup(sender, instance, created, **kwargs): def email_on_signup(sender, instance, created, **kwargs):
"""Send email validation."""
if created and instance.email: if created and instance.email:
# TODO: Possible bug due to many-to-many relationship with events and forms. # TODO: Possible bug due to many-to-many relationship with events and forms.
# TODO: Subject field crashes with lazy loaded translations. # TODO: Subject field crashes with lazy loaded translations.
@@ -164,118 +171,25 @@ class BaseRole(models.Model):
return '{} ({})'.format(n, _('board member')) if self.is_board else n return '{} ({})'.format(n, _('board member')) if self.is_board else n
class PresetRole(BaseRole): class JobAd(models.Model):
"""Model representing a preset occupation in the guild.""" """Job advertisements shown on Corporate relations page"""
description = models.TextField(_('Description'))
class Committee(models.Model):
"""
Committee model
Has many Roles found under variable roles
"""
class Meta: class Meta:
"""Meta class for Committee class.""" verbose_name = _('JobAd')
verbose_name_plural = _('JobAds')
verbose_name = _('Committee') title = models.CharField(max_length=255)
verbose_name_plural = _('Committees') description = models.CharField(max_length=255)
content = models.TextField()
def __str__(self): visible = models.BooleanField(default=True)
return _('Committee: {}').format(self.name) created_at = models.DateTimeField(default=timezone.now)
autohide_at = models.DateTimeField(default=month_from_now)
name = models.CharField(_("Name"), max_length=255) autohide_enabled = models.BooleanField(default=False)
@property
def current_roles(self):
return self.roles.all().filter(end_date__gte=timezone.now()).filter(start_date__lte=timezone.now())
class Role(PresetRole):
"""
Model for Role.
Model representing an active or historical occupation
in the guild.
"""
class Meta:
"""Meta class for Role model."""
verbose_name = _('Role')
verbose_name_plural = _('Roles')
committee = models.ForeignKey('Committee', related_name='roles', on_delete=models.SET_NULL, null=True)
def __str__(self):
return '{} (Hallitus: {}) ({})'.format(self.name, _("Yes") if self.is_board else _("No"), self.committee)
class Occupation(models.Model):
"""
Model for a occupation in guild.
Model links Official into a role he/she has or has had in the guild.
"""
class Meta:
verbose_name = _('Occupation')
verbose_name_plural = _('Occupations')
start_date = models.DateField(_('Start date'))
end_date = models.DateField(_('End date'))
role = models.ForeignKey('Role', on_delete=models.SET_NULL, null=True)
@staticmethod
def by_year(year):
return Occupation.objects.filter(
end_date__gte=timezone.datetime(year, 1, 1)).filter(
start_date__lte=timezone.datetime(year, 12, 31))
def __str__(self):
return '{}: {} - {}'.format(self.role.name, self.start_date, self.end_date)
class Official(models.Model):
"""Model representing a guild official."""
class Meta:
"""Meta class for Official class."""
verbose_name = _('Official')
verbose_name_plural = _('Officials')
user = models.OneToOneField(User, on_delete=models.CASCADE)
first_name = models.CharField(_('First name'), max_length=30)
last_name = models.CharField(_('Last name'), max_length=150)
email = models.EmailField(_('Email address'))
phone_number = PhoneNumberField(_('Phone number'))
role_history = models.ManyToManyField('Occupation', 'officials', blank=True)
image = models.ImageField(blank=True, null=True)
@staticmethod
def by_year(year):
return Official.objects.filter(
role_history__in=Occupation.occupations_by_year(year)).distinct()
def __str__(self):
return '{} {}'.format(self.first_name, self.last_name)
@receiver(post_save, sender=Official)
def save_user_official(sender, instance, **kwargs):
instance.user.first_name = instance.first_name
instance.user.last_name = instance.last_name
instance.user.email = instance.email
instance.user.save()
auditlog.register(Tag) auditlog.register(Tag)
auditlog.register(Feed) auditlog.register(Feed)
auditlog.register(Event) auditlog.register(Event)
auditlog.register(SignupForm)
auditlog.register(Signup) auditlog.register(Signup)
auditlog.register(PresetRole) auditlog.register(JobAd)
auditlog.register(Role)
auditlog.register(Official)
+3 -27
View File
@@ -141,31 +141,7 @@ class FeedSerializer(serializers.ModelSerializer):
return feed return feed
class CommitteeSerializer(serializers.ModelSerializer): class JobAdSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = Committee model = JobAd
fields = ['name_fi', 'name_en'] fields = ('id', 'title_fi', 'title_en', 'description_fi', 'description_en', 'content_fi', 'content_en', 'visible', 'autohide_at', 'autohide_enabled')
class RoleSerializer(serializers.ModelSerializer):
committee = CommitteeSerializer(read_only=True)
class Meta:
model = Role
fields = ('name_fi', 'name_en', 'description_fi', 'description_en', 'committee', 'is_board')
class ContactsSerializer(serializers.ModelSerializer):
class Meta:
model = Official
fields = ('first_name', 'last_name', 'email', 'phone_number', 'image')
depth = 2
class OccupationSerializer(serializers.ModelSerializer):
role = RoleSerializer(read_only=True)
officials = ContactsSerializer(many=True, read_only=True)
class Meta:
model = Occupation
fields = ('role', 'start_date', 'end_date', 'officials')
+3 -2
View File
@@ -40,14 +40,15 @@ CBOX_SCHEMA = {
} }
def createSignupForm(name="Form1", start_time=timezone.now(), end_time=month_from_now(), questions=ALL_QUESTION_TYPES, schema=ALL_QUESTIONS_SCHEMA, visible=True): def createSignupForm(name="Form1", start_time=timezone.now(), end_time=month_from_now(), questions=ALL_QUESTION_TYPES, schema=ALL_QUESTIONS_SCHEMA, visible=True, quota=1):
return SignupForm.objects.create( return SignupForm.objects.create(
title=name, title=name,
start_time=start_time, start_time=start_time,
end_time=end_time, end_time=end_time,
questions=questions, questions=questions,
visible=visible, visible=visible,
schema=schema schema=schema,
quota=quota
) )
-98
View File
@@ -1,98 +0,0 @@
from django.test import TestCase
from django.contrib.auth.models import User
from django.utils import timezone
from rest_framework import status
from rest_framework.test import APITestCase
from webapp.models import Official, Role, Occupation, Committee
from webapp.serializers import OccupationSerializer
URL = "/api/contacts/"
COMMITTEE = Committee.objects.create(
name_fi="Viestintä",
name_en="Communications"
)
def createRoleBoard():
return Role.objects.create(
name_fi="Metsuri",
name_en="The lumberjack",
is_board=True,
description_fi="Toimikunta PJ",
description_en="Committee Chair"
)
def createRoleNoBoard():
return Role.objects.create(
name_fi="Toimari",
name_en="Official",
is_board=False,
description_fi="Toimikunta jäbä",
description_en="Committee dude(tte)",
committee=COMMITTEE
)
def createOccupation(year, role=createRoleNoBoard(), dummydata=1):
occupation = Occupation.objects.create(
start_date=timezone.datetime(year, 1, 1),
end_date=timezone.datetime(year, 12, 31),
role=role
)
occupation.officials.add(
createPerson(dummydata)
)
return occupation
def createPerson(name):
return Official.objects.create(
user=User.objects.create_user(f"testi{name}", "test@test.tld", "password123"),
first_name=f"first{name}",
last_name=f"last{name}",
email="test@test.tld",
phone_number="+358501234567",
image=""
)
class ContactsTestCase(APITestCase):
current_year = timezone.now().year
old_year = 1970
def setUp(self):
createOccupation(self.current_year, role=createRoleBoard(), dummydata=1)
createOccupation(self.current_year, dummydata=2)
createOccupation(self.old_year, role=createRoleBoard(), dummydata=3)
createOccupation(self.old_year, dummydata=4)
def test_get(self):
response = self.client.get(f"{URL}", format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)
serializer = OccupationSerializer(
Occupation.by_year(self.current_year),
many=True
)
self.assertEqual(response.data["results"], serializer.data)
def test_get_by_year(self):
response = self.client.get(f"{URL}?year={self.old_year}", format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)
serializer = OccupationSerializer(
Occupation.by_year(self.old_year),
many=True
)
self.assertEqual(response.data["results"], serializer.data)
def test_by_year_empty(self):
response = self.client.get(f"{URL}?year=1971")
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data["results"], [])
+5 -7
View File
@@ -27,11 +27,9 @@ class FeedTestCase(APITestCase):
self.assertTrue(status.is_success(response.status_code)) self.assertTrue(status.is_success(response.status_code))
feeds = Feed.objects.all() feeds = Feed.objects.all()
serializer = FeedSerializer( serializer = FeedSerializer(feeds, many=True, context={
feeds, many=True, "request": APIRequestFactory().get(r"http://testserver/api/feed/")
context={ })
"request": APIRequestFactory().get(r"http://testserver/api/events/")
})
self.assertEqual(response.data["results"], serializer.data) self.assertEqual(response.data["results"], serializer.data)
def test_post_feed(self): def test_post_feed(self):
@@ -50,13 +48,13 @@ class FeedTestCase(APITestCase):
} }
# Try post without authentication # Try post without authentication
response = self.client.post("/api/feed/", data, format="json") response = self.client.post("/api/feed/", data, format="json")
self.assertTrue(response.status_code, status.HTTP_401_UNAUTHORIZED) self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
self.assertEqual(Feed.objects.count(), 1) self.assertEqual(Feed.objects.count(), 1)
# Authenticate # Authenticate
self.client.force_authenticate(user=self.authClient) self.client.force_authenticate(user=self.authClient)
response = self.client.post("/api/feed/", data, format="json") response = self.client.post("/api/feed/", data, format="json")
# Return success and check object was created # Return success and check object was created
self.assertTrue(response.status_code, status.HTTP_201_CREATED) self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertEqual(Feed.objects.count(), 2) self.assertEqual(Feed.objects.count(), 2)
created = Feed.objects.get(title_fi="testtitle") created = Feed.objects.get(title_fi="testtitle")
+54
View File
@@ -0,0 +1,54 @@
from django.contrib.auth.models import User
from rest_framework import status
from rest_framework.test import APITestCase, APIRequestFactory
from webapp.models import JobAd
from webapp.serializers import JobAdSerializer
API = "/api/jobads/"
class JobAdTestCase(APITestCase):
def setUp(self):
self.prefilled_jobad = JobAd.objects.create(
title_fi="ABB Test",
title_en="ABB Test",
visible=True,
description_fi="desc",
description_en="desc",
content_fi="lorem",
content_en="lorem"
)
username, password = "test_admin", "password123"
self.authClient = User.objects.create_superuser(username, "myemail@test.com", password)
def test_get_jobads(self):
response = self.client.get(API, format="json")
expected = JobAdSerializer(self.prefilled_jobad).data
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data["results"][0], expected)
def test_post_jobad(self):
data = {
"title_fi": "testtitle",
"title_en": "testtitle",
"visible": "True",
"description_fi": "liirumlaarum",
"description_en": "liirumlaarum",
"content_fi": "lorem ipsum",
"content_en": "lorem ipsum",
"autohide_enabled": "True"
}
# Try post without authentication
response = self.client.post(API, data, format="json")
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
self.assertEqual(JobAd.objects.count(), 1)
# Authenticate
self.client.force_authenticate(user=self.authClient)
response = self.client.post(API, data, format="json")
# Return success and check object was created
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertEqual(JobAd.objects.count(), 2)
+32 -4
View File
@@ -56,16 +56,44 @@ class SignupTestCase(APITestCase):
self.assertEqual(response.status_code, status.HTTP_201_CREATED) self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertEqual(Signup.objects.count(), 3) self.assertEqual(Signup.objects.count(), 3)
def test_delete_as_admin(self):
id = self.signup1.id
no_auth_response = self.client.delete(f"{URL}{id}/", format="json")
self.assertEqual(no_auth_response.status_code, status.HTTP_401_UNAUTHORIZED)
self.client.force_authenticate(user=self.authClient)
response = self.client.delete(f"{URL}{id}/", format="json")
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(Signup.objects.get(id=id).deleted, True)
@skip("NotImplemented") @skip("NotImplemented")
def test_get_hidden_forms_admin(self): def test_get_hidden_forms_admin(self):
pass pass
# Update and Delete are available for super admin (Django Admin)
# and to the user that signed up (uid token)
@skip("NotImplemented")
def test_update_signup_token(self): def test_update_signup_token(self):
pass id = self.signup1.id
uuid = self.signup1.uuid
clone = ALL_QUESTION_TYPES_ANSWER.copy()
clone["-naY2R1-h"] = "Edited Testi"
new = createSignupRequest("asd", self.signupForm.id, clone)
response = self.client.put(f"{URL}{id}/edit/?uuid={uuid}", new, format="json")
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(Signup.objects.get(id=id).answer["-naY2R1-h"], "Edited Testi")
@skip("NotImplemented") @skip("NotImplemented")
def test_delete_signup_token(self): def test_delete_signup_token(self):
pass pass
# TODO: Use some mocking library and check that mailjet is actually called
def test_signupee_sendemail(self):
form = self.signupForm
emailURL = f"/api/signupForm/{form.id}/sendemail/"
payload = {
"subject": "Email subject",
"content": "Markdown",
"mode": "actual"
}
no_auth_response = self.client.post(emailURL, payload, format="json")
self.assertEqual(no_auth_response.status_code, status.HTTP_401_UNAUTHORIZED)
self.client.force_authenticate(user=self.authClient)
response = self.client.post(emailURL, payload, format="json")
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
+3 -33
View File
@@ -6,69 +6,39 @@ from webapp.models import *
@register(BaseFeed) @register(BaseFeed)
class BaseFeedTranslationOptions(TranslationOptions): class BaseFeedTranslationOptions(TranslationOptions):
"""Class for base feed translation options."""
fields = ('title', 'description', 'content') fields = ('title', 'description', 'content')
@register(Feed) @register(Feed)
class FeedTranslationOptions(TranslationOptions): class FeedTranslationOptions(TranslationOptions):
"""Class for feed translation options."""
fields = () fields = ()
@register(Tag) @register(Tag)
class TagTranslationOptions(TranslationOptions): class TagTranslationOptions(TranslationOptions):
"""Class for tag translation options."""
fields = ('name',) fields = ('name',)
@register(Event) @register(Event)
class EventTranslationOptions(TranslationOptions): class EventTranslationOptions(TranslationOptions):
"""Class for event translation options."""
fields = ('location',) fields = ('location',)
@register(Signup) @register(Signup)
class SignupTranslationOptions(TranslationOptions): class SignupTranslationOptions(TranslationOptions):
"""Class for registration translation options."""
fields = () fields = ()
@register(SignupForm) @register(SignupForm)
class SignupFormTranslationOptions(TranslationOptions): class SignupFormTranslationOptions(TranslationOptions):
"""Class for registration translation options."""
fields = ('title',) fields = ('title',)
@register(TemplateQuestion) @register(TemplateQuestion)
class TemplateQuestionTranslationOptions(TranslationOptions): class TemplateQuestionTranslationOptions(TranslationOptions):
"""Class for registration translation options."""
fields = () fields = ()
@register(BaseRole) @register(JobAd)
class BaseRoleTranslationOptions(TranslationOptions): class JobAdTranslationOptions(TranslationOptions):
"""Class for base role translation options""" fields = ('title', 'description', 'content',)
fields = ('name',)
@register(PresetRole)
class PresetRoleTranslationOptions(TranslationOptions):
"""Class for PresetRole translation options."""
fields = ('description',)
@register(Committee)
class CommitteeTranslationOptions(TranslationOptions):
"""Class for PresetRole translation options."""
fields = ('name',)
+1 -2
View File
@@ -18,10 +18,9 @@ router.register(r'events', EventViewSet)
router.register(r'signupForm', SignupFormViewSet) router.register(r'signupForm', SignupFormViewSet)
router.register(r'signup', SignupViewSet) router.register(r'signup', SignupViewSet)
router.register(r'feed', FeedViewSet) router.register(r'feed', FeedViewSet)
router.register(r'contacts', ContactsViewSet)
router.register(r'committees', CommitteeViewSet)
router.register(r'questions', SavedQuestionsViewSet) router.register(r'questions', SavedQuestionsViewSet)
router.register(r'tags', TagsViewSet) router.register(r'tags', TagsViewSet)
router.register(r'jobads', JobAdViewSet)
urlpatterns = [ urlpatterns = [
url(r'^api/', include(router.urls)), url(r'^api/', include(router.urls)),
+9
View File
@@ -55,6 +55,10 @@ def month_from_now():
def send_email(to, subject, body, html=False): def send_email(to, subject, body, html=False):
if not ENABLE_AUTOMATIC_EMAILS: if not ENABLE_AUTOMATIC_EMAILS:
logging.debug("Skipping email")
logging.debug(f"to: {to}")
logging.debug(f"subject: {subject}")
logging.debug(f"body: {body}")
return return
try: try:
mailjet = Client(auth=(EMAIL_API_KEY, EMAIL_API_SECRET), version='v3.1') mailjet = Client(auth=(EMAIL_API_KEY, EMAIL_API_SECRET), version='v3.1')
@@ -101,3 +105,8 @@ def send_signup_email(to, subject, id, uuid, content):
) )
return send_email(to, subject, message, True) return send_email(to, subject, message, True)
def admin_send_email_signupees(list, subject, content):
for to in list:
send_email(to, subject, markdown.markdown(content), True)
+54 -25
View File
@@ -12,18 +12,17 @@ from django_filters import rest_framework as filters
from django.db.models import Prefetch from django.db.models import Prefetch
from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ObjectDoesNotExist
from rest_framework import routers from rest_framework import routers
from rest_framework.response import Response
from rest_framework.viewsets import ModelViewSet, ReadOnlyModelViewSet from rest_framework.viewsets import ModelViewSet, ReadOnlyModelViewSet
from rest_framework.filters import OrderingFilter, SearchFilter from rest_framework.filters import OrderingFilter, SearchFilter
from rest_framework.decorators import action from rest_framework.decorators import action
from rest_framework.permissions import IsAuthenticatedOrReadOnly, BasePermission, AllowAny from rest_framework.permissions import IsAuthenticatedOrReadOnly, BasePermission, AllowAny, IsAuthenticated
from jsonschema import validate from jsonschema import validate
from jsonschema.exceptions import ValidationError from jsonschema.exceptions import ValidationError
from webapp.models import Event, SignupForm, Signup, TemplateQuestion, Feed, Committee, Occupation, Tag from webapp.models import *
from webapp.serializers import (EventSerializer, SignupFormSerializer, SignupSerializer, from webapp.serializers import *
SavedQuestionsSerializer, FeedSerializer, CommitteeSerializer, from webapp.utils import admin_send_email_signupees, decode_base64_file
OccupationSerializer, TagSerializer)
from webapp.utils import decode_base64_file
class SignupPermission(BasePermission): class SignupPermission(BasePermission):
@@ -106,9 +105,37 @@ class SignupFormViewSet(ModelViewSet):
return SignupForm.objects.all().order_by('start_time') return SignupForm.objects.all().order_by('start_time')
return SignupForm.objects.filter(visible=True, end_time__gt=timezone.now()).order_by('start_time') return SignupForm.objects.filter(visible=True, end_time__gt=timezone.now()).order_by('start_time')
@action(detail=True, methods=['post'], permission_classes=[IsAuthenticated])
def sendemail(self, request, pk=None, *args, **kwargs):
subject = request.data["subject"]
content = request.data["content"]
mode = request.data["mode"]
queryset = self.filter_queryset(self.get_queryset())
filter = {'pk': pk}
signupForm = get_object_or_404(queryset, **filter)
if (mode == "all"):
admin_send_email_signupees(signupForm.signups, subject, content)
return JsonResponse(status=201, data={"message": "Email sent"})
elif (mode == "actual"):
admin_send_email_signupees(signupForm.signups[:signupForm.quota], subject, content)
return JsonResponse(status=201, data={"message": "Email sent"})
elif (mode == "reserved"):
admin_send_email_signupees(signupForm.signups[signupForm.quota:], subject, content)
return JsonResponse(status=201, data={"message": "Email sent"})
else:
return JsonResponse(status=400, data={"error": f"Bad mode '{mode}'"})
@action(detail=True, methods=['get'], permission_classes=[IsAuthenticated])
def signups(self, request, pk=None, *args, **kwargs):
queryset = self.filter_queryset(self.get_queryset())
filter = {'pk': pk}
signupForm = get_object_or_404(queryset, **filter)
return Response(SignupSerializer(signupForm.signups, many=True).data)
class SignupViewSet(ModelViewSet): class SignupViewSet(ModelViewSet):
queryset = Signup.objects.all() queryset = Signup.objects.filter(deleted=False)
serializer_class = SignupSerializer serializer_class = SignupSerializer
permission_classes = [SignupPermission] permission_classes = [SignupPermission]
@@ -158,6 +185,15 @@ class SignupViewSet(ModelViewSet):
else: else:
return JsonResponse(status=404, data={"error": f"SignupForm {id} not found"}) return JsonResponse(status=404, data={"error": f"SignupForm {id} not found"})
def destroy(self, request, pk=None, *args, **kwargs):
try:
signup = self.get_object()
signup.deleted = True
signup.save()
return JsonResponse(status=200, data={"message": "OK"})
except ObjectDoesNotExist:
return JsonResponse(status=404, data={"error": f"Signup {pk} not found"})
class SavedQuestionsViewSet(ModelViewSet): class SavedQuestionsViewSet(ModelViewSet):
queryset = TemplateQuestion.objects.all() queryset = TemplateQuestion.objects.all()
@@ -191,30 +227,23 @@ class FeedViewSet(ModelViewSet):
return Feed.objects.filter(id__in=result_ids) return Feed.objects.filter(id__in=result_ids)
class ContactsViewSet(ReadOnlyModelViewSet):
queryset = Occupation.objects.all()
serializer_class = OccupationSerializer
permission_classes = [IsAuthenticatedOrReadOnly]
def get_queryset(self):
year = self.request.query_params.get('year')
if not year:
return Occupation.by_year(timezone.now().year)
return Occupation.by_year(int(year))
class CommitteeViewSet(ReadOnlyModelViewSet):
queryset = Committee.objects.all()
serializer_class = CommitteeSerializer
permission_classes = [IsAuthenticatedOrReadOnly]
class TagsViewSet(ReadOnlyModelViewSet): class TagsViewSet(ReadOnlyModelViewSet):
queryset = Tag.objects.all() queryset = Tag.objects.all()
serializer_class = TagSerializer serializer_class = TagSerializer
permission_classes = [IsAuthenticatedOrReadOnly] permission_classes = [IsAuthenticatedOrReadOnly]
class JobAdViewSet(ModelViewSet):
queryset = JobAd.objects.all()
serializer_class = JobAdSerializer
permission_classes = [IsAuthenticatedOrReadOnly]
def get_queryset(self):
if self.request.user.is_authenticated:
return JobAd.objects.all()
return JobAd.objects.filter(visible=True, autohide_at__gt=timezone.now())
@require_http_methods(["GET"]) @require_http_methods(["GET"])
def about_view(request, *args, **kwargs): def about_view(request, *args, **kwargs):
"""Render about page.""" """Render about page."""