Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feature/BE-30: Exhibitions #344

Merged
merged 28 commits into from
Dec 19, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
968cfe8
Date time format is updated
KarahanS Dec 7, 2022
98bab9a
Fix time zone and datetime format
KarahanS Dec 7, 2022
92c4981
update documentation to fix date format
KarahanS Dec 7, 2022
968e72e
Change updated_at field when the profile is updated
KarahanS Dec 8, 2022
a64b902
minor fixes on artitem documentation
KarahanS Dec 8, 2022
109c4a8
exhibition models are added
KarahanS Dec 8, 2022
d038f24
exhibition serializers are added
KarahanS Dec 8, 2022
623aea5
add api urls and configure admin settings
KarahanS Dec 8, 2022
c18d6af
make migrations to include exhibitions
KarahanS Dec 8, 2022
23ee25a
added unit tests to test helper functions
KarahanS Dec 8, 2022
b2b39f3
Merge branch 'master' into feature/BE-30
KarahanS Dec 9, 2022
582a16f
Add extra field for the status of the exhibition, also add a constrai…
KarahanS Dec 14, 2022
284b7ff
Merge branch 'feature/BE-30' of https://github.com/bounswe/bounswe202…
BElifb Dec 16, 2022
7ce3c3c
Implemented PUT for virtual exhibitions, combined PUT, GET and DELETE…
KarahanS Dec 17, 2022
f9b8029
Migrations
BElifb Dec 17, 2022
a2d8bad
Updated exhibition documentation - replaced 'type' with 'category'
KarahanS Dec 18, 2022
24611e4
Merge branch 'master' into feature/BE-30
KarahanS Dec 18, 2022
c57869e
Merge branch 'feature/BE-30' of https://github.com/bounswe/bounswe202…
BElifb Dec 18, 2022
1bd5707
Merge branch 'feature/BE-30' of https://github.com/bounswe/bounswe202…
BElifb Dec 18, 2022
61f8935
Small correction in SimpleArtItemSerializer
BElifb Dec 18, 2022
75bcc08
Fixed category conflict
KarahanS Dec 18, 2022
ce6ba3e
Fix Swagger for 'ongoing' status
KarahanS Dec 18, 2022
c7d0c55
Merge branch 'master' into feature/BE-30
KarahanS Dec 18, 2022
537835a
Add 'number_of_views' field to Swagger doc and remigrate
KarahanS Dec 18, 2022
fad878f
Fix list index out of range error
KarahanS Dec 18, 2022
9a96c98
Saved object at update virtual exhibition API
BElifb Dec 18, 2022
e4c772a
Corrected try-except block
KarahanS Dec 19, 2022
ec4b87a
fix typoe in exhibitions
KarahanS Dec 19, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 11 additions & 2 deletions App/backend/api/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from .models.models import Comment
from .models.artitem import Tag, ArtItem
from .models.user import User

from .models.exhibition import OfflineExhibition, VirtualExhibition

class UserAdmin(admin.ModelAdmin):
# exclude = ('otp',)
Expand All @@ -17,7 +17,16 @@ class CommentAdmin(admin.ModelAdmin):
class ArtItemAdmin(admin.ModelAdmin):
list_display = ['title', 'id', 'description', 'owner', 'artitem_image']

class OfflineExhibitionAdmin(admin.ModelAdmin):
list_display = ['id', 'owner', 'title', 'description', 'poster', 'start_date', 'end_date', 'created_at', 'updated_at', 'city', 'country', 'address', 'latitude', 'longitude']

class VirtualExhibitionAdmin(admin.ModelAdmin):
list_display = ['id', 'owner', 'title', 'description', 'poster', 'start_date', 'end_date', 'created_at', 'updated_at']


admin.site.register(User, UserAdmin)
admin.site.register(Tag, TagAdmin)
admin.site.register(Comment, CommentAdmin)
admin.site.register(ArtItem, ArtItemAdmin)
admin.site.register(ArtItem, ArtItemAdmin)
admin.site.register(OfflineExhibition, OfflineExhibitionAdmin)
admin.site.register(VirtualExhibition, VirtualExhibitionAdmin)
60 changes: 58 additions & 2 deletions App/backend/api/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Generated by Django 4.1.2 on 2022-12-17 14:45
# Generated by Django 4.1.2 on 2022-12-18 12:31

from django.conf import settings
import django.contrib.auth.models
Expand Down Expand Up @@ -59,10 +59,11 @@ class Migration(migrations.Migration):
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('title', models.CharField(max_length=200)),
('description', models.CharField(max_length=500)),
('category', models.CharField(choices=[('AR', 'Architecture'), ('SC', 'Sculpture'), ('DR', 'Drawing'), ('PH', 'Photography'), ('PR', 'Prints'), ('PA', 'Painting/Acrylic'), ('PO', 'Painting Oilpaint'), ('PW', 'Painting Watercolour'), ('PD', 'Painting Digital'), ('PM', 'Painting Mural'), ('PG', 'Painting Gouache'), ('PP', 'Painting Pastel'), ('PE', 'Painting Encaustic'), ('PF', 'Painting Fresco'), ('PS', 'Painting Spray'), ('OP', 'Painting Other'), ('Other', 'Other')], default='Other', max_length=20)),
('category', models.CharField(choices=[('AR', 'Architecture'), ('SC', 'Sculpture'), ('SK', 'Sketch'), ('DR', 'Drawing'), ('PT', 'Poster'), ('PH', 'Photography'), ('PR', 'Prints'), ('PA', 'Painting/Acrylic'), ('PO', 'Painting Oilpaint'), ('PW', 'Painting Watercolour'), ('PD', 'Painting Digital'), ('PM', 'Painting Mural'), ('PG', 'Painting Gouache'), ('PP', 'Painting Pastel'), ('PE', 'Painting Encaustic'), ('PF', 'Painting Fresco'), ('PS', 'Painting Spray'), ('OP', 'Painting Other'), ('OT', 'Other')], default='OT', max_length=2)),
('artitem_image', models.ImageField(default='artitem/defaultart.jpg', upload_to='artitem/')),
('artitem_path', models.TextField(default='artitem/defaultart.jpg')),
('created_at', models.DateTimeField(auto_now_add=True)),
('number_of_views', models.IntegerField(default=0)),
('owner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
options={
Expand Down Expand Up @@ -98,6 +99,48 @@ class Migration(migrations.Migration):
('updated_at', models.DateTimeField(auto_now=True)),
],
),
migrations.CreateModel(
name='VirtualExhibition',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('title', models.CharField(max_length=200)),
('description', models.CharField(max_length=500)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('start_date', models.DateTimeField()),
('end_date', models.DateTimeField()),
('artitems_gallery', models.ManyToManyField(blank=True, related_name='gallery', to='api.artitem')),
('collaborators', models.ManyToManyField(blank=True, related_name='virtualCollaborators', to=settings.AUTH_USER_MODEL)),
('owner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
('poster', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='api.artitem')),
],
options={
'ordering': ['-created_at'],
},
),
migrations.CreateModel(
name='OfflineExhibition',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('title', models.CharField(max_length=200)),
('description', models.CharField(max_length=500)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('start_date', models.DateTimeField()),
('end_date', models.DateTimeField()),
('city', models.CharField(blank=True, max_length=200, null=True)),
('country', models.CharField(blank=True, max_length=200, null=True)),
('address', models.CharField(blank=True, max_length=200, null=True)),
('latitude', models.FloatField()),
('longitude', models.FloatField()),
('collaborators', models.ManyToManyField(blank=True, related_name='offlineCollaborators', to=settings.AUTH_USER_MODEL)),
('owner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
('poster', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='api.artitem')),
],
options={
'ordering': ['-created_at'],
},
),
migrations.CreateModel(
name='LikeComment',
fields=[
Expand Down Expand Up @@ -139,6 +182,19 @@ class Migration(migrations.Migration):
name='tags',
field=models.ManyToManyField(blank=True, to='api.tag'),
),
migrations.AddField(
model_name='artitem',
name='virtualExhibition',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='api.virtualexhibition'),
),
migrations.AddConstraint(
model_name='virtualexhibition',
constraint=models.CheckConstraint(check=models.Q(('end_date__gt', models.F('start_date'))), name='api_virtualexhibition has valid start-end dates.'),
),
migrations.AddConstraint(
model_name='offlineexhibition',
constraint=models.CheckConstraint(check=models.Q(('end_date__gt', models.F('start_date'))), name='api_offlineexhibition has valid start-end dates.'),
),
migrations.AddConstraint(
model_name='likecomment',
constraint=models.UniqueConstraint(fields=('user', 'comment'), name='api_likecomment_unique_relationships'),
Expand Down
18 changes: 0 additions & 18 deletions App/backend/api/migrations/0002_artitem_number_of_views.py

This file was deleted.

18 changes: 0 additions & 18 deletions App/backend/api/migrations/0003_alter_artitem_category.py

This file was deleted.

6 changes: 5 additions & 1 deletion App/backend/api/models/artitem.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,11 @@
from django.contrib.auth.models import AbstractUser
from django.conf import settings
from .user import User

from django.template.defaultfilters import date
from django.utils.translation import gettext_lazy as _


class Tag(models.Model):
tagname = models.CharField(max_length=100)
description = models.CharField(max_length=500) # description about the tag
Expand Down Expand Up @@ -45,6 +48,7 @@ class Category(models.TextChoices):
artitem_image = models.ImageField( default='artitem/defaultart.jpg', upload_to='artitem/')
artitem_path = models.TextField(default= 'artitem/defaultart.jpg')
created_at = models.DateTimeField(auto_now_add=True)
virtualExhibition = models.ForeignKey('api.VirtualExhibition', on_delete=models.CASCADE, blank=True, null=True)
number_of_views = models.IntegerField(default=0)

def increaseViews(self, *args, **kwargs):
Expand All @@ -53,7 +57,7 @@ def increaseViews(self, *args, **kwargs):

class Meta:
ordering = ["-created_at"] # order according to the timestamps

def __str__(self):
return "Art item: " + self.title

Expand Down
66 changes: 66 additions & 0 deletions App/backend/api/models/exhibition.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
from email.policy import default
from django.db import models
from django.contrib.auth.models import AbstractUser
from django.conf import settings
from .user import User
from ..models.artitem import ArtItem
import datetime
from django.utils import timezone
from django.db.models import F, Q


class AbstractExhibition(models.Model):
title = models.CharField(max_length=200)
description = models.CharField(max_length=500)
owner = models.ForeignKey(User, on_delete=models.CASCADE)
poster = models.OneToOneField(ArtItem, on_delete=models.CASCADE) # a poster must be unique to an exhibition and each exhibition must have one
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
start_date = models.DateTimeField()
end_date = models.DateTimeField()

class Meta:
ordering = ["-created_at"] # order according to the timestamp
abstract = True

@property
def get_status(self):
currentTime = timezone.now()
if(self.start_date > currentTime): return "Not Started Yet"
elif(self.end_date < currentTime): return "Finished"
else: return "Ongoing"

def __str__(self):
return "Exhibition: " + self.title

class VirtualExhibition(AbstractExhibition):
artitems_gallery = models.ManyToManyField(ArtItem, related_name="gallery", blank=True) # select an art item from gallery - nothing happens to art item if the exhibition is deleted
collaborators = models.ManyToManyField(User, related_name="virtualCollaborators", blank=True) # it's not compulsory to have collaborators
class Meta:
ordering = ["-created_at"] # order according to the timestamp
constraints = [
models.CheckConstraint(
check=Q(end_date__gt=F('start_date')),
name = "%(app_label)s_%(class)s has valid start-end dates."
),
]

@property
def get_uploaded_artitems(self):
return ArtItem.objects.filter(virtualExhibition=self)

class OfflineExhibition(AbstractExhibition):
city = models.CharField(max_length=200,blank=True, null=True)
country = models.CharField(max_length=200,blank=True, null=True)
address = models.CharField(max_length=200,blank=True, null=True)
latitude = models.FloatField()
longitude = models.FloatField()
collaborators = models.ManyToManyField(User, related_name="offlineCollaborators", blank=True) # it's not compulsory to have collaborators
class Meta:
ordering = ["-created_at"] # order according to the timestamp
constraints = [
models.CheckConstraint(
check=Q(end_date__gt=F('start_date')),
name = "%(app_label)s_%(class)s has valid start-end dates."
),
]
62 changes: 62 additions & 0 deletions App/backend/api/serializers/exhibition.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
from dataclasses import fields
from pyexpat import model
from rest_framework import serializers
from ..models.models import Comment
from ..models.artitem import Tag, ArtItem
from ..models.user import User
from .serializers import TagSerializer, SimpleUserSerializer, ArtItemSerializer, SimpleArtItemSerializer
from ..models.exhibition import OfflineExhibition, VirtualExhibition

class OfflineExhibitionSerializer(serializers.ModelSerializer):
status = serializers.ReadOnlyField(source='get_status')

class Meta:
model = OfflineExhibition
fields = ['id', 'owner', 'title', 'description', 'poster', 'collaborators', 'start_date', 'end_date', 'created_at', 'updated_at',
'city', 'country', 'address', 'latitude', 'longitude', 'status']

def to_representation(self, instance):
rep = super().to_representation(instance)
rep["poster"] = SimpleArtItemSerializer(instance.poster).data
rep["collaborators"] = SimpleUserSerializer(instance.collaborators, many=True).data
rep["owner"] = SimpleUserSerializer(instance.owner).data
return rep

class SimpleExhibitionArtItemSerializer(serializers.ModelSerializer):
likes = serializers.ReadOnlyField(source='get_numberof_likes')
class Meta:
model = ArtItem
fields = ['id', 'title', 'tags', 'description', 'category', 'artitem_path', 'likes', 'created_at']

def to_representation(self, instance):
rep = super().to_representation(instance)
rep["tags"] = TagSerializer(instance.tags.all(), many=True).data
return rep

class ExhibitionArtItemSerializer(serializers.ModelSerializer):
likes = serializers.ReadOnlyField(source='get_numberof_likes')
class Meta:
model = ArtItem
fields = ['id', 'owner', 'title', 'tags', 'description', 'category', 'virtualExhibition', 'artitem_path', 'artitem_image', 'likes', 'created_at']

def to_representation(self, instance):
rep = super().to_representation(instance)
rep["tags"] = TagSerializer(instance.tags.all(), many=True).data
rep["owner"] = SimpleUserSerializer(instance.owner).data
return rep

class VirtualExhibitionSerializer(serializers.ModelSerializer):
status = serializers.ReadOnlyField(source='get_status')
artitems_upload = serializers.ReadOnlyField(source='get_uploaded_artitems')
class Meta:
model = VirtualExhibition
fields = ['id', 'owner', 'title', 'description', 'poster', 'collaborators', 'artitems_gallery', 'start_date', 'end_date', 'created_at', 'updated_at', 'status', 'artitems_upload']

def to_representation(self, instance):
rep = super().to_representation(instance)
rep["poster"] = SimpleArtItemSerializer(instance.poster).data
rep["collaborators"] = SimpleUserSerializer(instance.collaborators, many=True).data
rep["owner"] = SimpleUserSerializer(instance.owner).data
rep["artitems_gallery"] = SimpleArtItemSerializer(instance.artitems_gallery, many=True).data
rep["artitems_upload"] = SimpleExhibitionArtItemSerializer(rep['artitems_upload'], many=True).data
return rep
2 changes: 1 addition & 1 deletion App/backend/api/serializers/profile.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,4 @@ class UserUpdateProfileSerializer(serializers.ModelSerializer):
# here, we define the parameters we expect to receive (not directly related to model)
class Meta:
model = User
fields = ['id', 'name', 'surname', 'about', 'location', 'profile_path']
fields = ['id', 'name', 'surname', 'about', 'location', 'profile_path', 'updated_at']
10 changes: 10 additions & 0 deletions App/backend/api/serializers/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,16 @@ def to_representation(self, instance):
rep["owner"] = SimpleUserSerializer(instance.owner).data
return rep

class SimpleArtItemSerializer(serializers.ModelSerializer):

class Meta:
model = ArtItem
fields = ['id', 'owner', 'title', 'description', 'category', 'tags', 'artitem_path', 'created_at']

def to_representation(self, instance):
rep = super().to_representation(instance)
rep["tags"] = TagSerializer(instance.tags.all(), many=True).data
return rep

class CommentSerializer(serializers.ModelSerializer):
class Meta:
Expand Down
61 changes: 61 additions & 0 deletions App/backend/api/tests/test_exhibitions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
from django.test import TestCase
from faker import Faker

from ..models.user import User
from ..models.artitem import ArtItem
from ..models.exhibition import ExhibitionArtItem
from ..serializers.serializers import ArtItem, ArtItemSerializer
from ..views.exhibition import validate_ids, fetch_image
from ..utils import ArtItemStorage
from django.core.files.base import ContentFile
"""
setUp() function is like a constructor for our test. It creates some mock data.
Thanks to the django.test, mock data is deleted automatically after the test, you do not have to worry about your database.
tearDown() function is like a destructor, it deletes the objects.
"""
test_base64 = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVQYV2NgYAAAAAMAAWgmWQ0AAAAASUVORK5CYII="

class ArtItemTest(TestCase):
# preparing to test
def setUp(self):
# setting up for the test
print("TestExhibition:setUp_:begin")
self.faker = Faker()

# do something
print("TestExhibition:setUp_:end")

def test_id_validation(self):
# test the helper function that checks if given IDs (artitem_gallery) are valid.
user1 = User.objects.create(username = self.faker.unique.word(), password = self.faker.password(), email = f"{self.faker.first_name()}.{self.faker.last_name()}@{self.faker.domain_name()}")
user2 = User.objects.create(username = self.faker.unique.word(), password = self.faker.password(), email = f"{self.faker.first_name()}.{self.faker.last_name()}@{self.faker.domain_name()}")

artitem1 = ArtItem.objects.create(title= self.faker.word(), description = self.faker.paragraph(nb_sentences=3), owner = user1) # id = 4
artitem2 = ArtItem.objects.create(title= self.faker.word(), description = self.faker.paragraph(nb_sentences=3), owner = user1) # id = 5
artitem3 = ArtItem.objects.create(title= self.faker.word(), description = self.faker.paragraph(nb_sentences=3), owner = user1) # id = 6
artitem4 = ArtItem.objects.create(title= self.faker.word(), description = self.faker.paragraph(nb_sentences=3), owner = user2) # id = 7
artitem5 = ArtItem.objects.create(title= self.faker.word(), description = self.faker.paragraph(nb_sentences=3), owner = user2) # id = 8

self.assertTrue(validate_ids([4, 5], user1.id))
self.assertTrue(validate_ids([6], user1.id))
self.assertTrue(validate_ids([7, 8], user2.id))
self.assertFalse(validate_ids([2, 4], user1.id))
self.assertFalse(validate_ids([2, 4], user2.id))
self.assertFalse(validate_ids([9], user2.id))

def test_fetch_image(self):
# test fetch_image function

artitem_image_storage = ArtItemStorage()
artitemdata = {}
artitem_data = fetch_image(artitemdata, artitem_image_storage, test_base64)

self.assertEqual(artitem_data["artitem_path"], "artitem/artitem-1.png")
self.assertTrue(isinstance(artitem_data["artitem_image"], ContentFile))

def tearDown(self):
# cleaning up after the test
print("TestExhibition:setUp_:begin")

# do something
print("TestExhibition:setUp_:end")
Loading