diff --git a/core/settings_base.py b/core/settings_base.py index 916c8ef..cd9dc92 100644 --- a/core/settings_base.py +++ b/core/settings_base.py @@ -37,6 +37,7 @@ "custom_auth", "allauth", "allauth.account", + "sponsors", ] MIDDLEWARE = [ @@ -75,7 +76,9 @@ STORAGES = { - # ... + "default": { + "BACKEND": "django.core.files.storage.FileSystemStorage" + }, "staticfiles": { "BACKEND": "whitenoise.storage.CompressedManifestStaticFilesStorage", }, @@ -100,6 +103,12 @@ }, ] +# MEDIA +# ------------------------------------------------------------------------------ +# https://docs.djangoproject.com/en/dev/ref/settings/#media-root +MEDIA_ROOT = BASE_DIR / "media" +# https://docs.djangoproject.com/en/dev/ref/settings/#media-url +MEDIA_URL = "/media/" # Internationalization # https://docs.djangoproject.com/en/3.2/topics/i18n/ @@ -166,19 +175,16 @@ "signup": "custom_auth.forms.CustomSignupForm", } -SITE_ID = 1 # new ACCOUNT_EMAIL_VERIFICATION = True ACCOUNT_EMAIL_REQUIRED = True ACCOUNT_EMAIL_CONFIRMATION_EXPIRE_DAYS = 1 -ACCOUNT_EMAIL_REQUIRED = True ACCOUNT_USER_MODEL_USERNAME_FIELD = None -ACCOUNT_EMAIL_REQUIRED = True ACCOUNT_USERNAME_REQUIRED = False ACCOUNT_AUTHENTICATION_METHOD = "email" ACCOUNT_UNIQUE_EMAIL = True ACCOUNT_LOGOUT_REDIRECT_URL = "/accounts/login/" LOGIN_REDIRECT_URL = "/" -ACCOUNT_LOGIN_ON_PASSWORD_RESET = True # logged automatiquely when success +ACCOUNT_LOGIN_ON_PASSWORD_RESET = True # logged automatically when success ACCOUNT_LOGOUT_ON_GET = True diff --git a/core/urls.py b/core/urls.py index 577cbf2..8b14195 100644 --- a/core/urls.py +++ b/core/urls.py @@ -13,9 +13,10 @@ 1. Import the include() function: from django.urls import include, path 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) """ - +from django.conf import settings from django.conf.urls.i18n import i18n_patterns from django.contrib import admin +from django.conf.urls.static import static from django.urls import path, include @@ -23,14 +24,19 @@ path("", include("website.urls")), path("accounts/", include("allauth.urls")), path("proposals/", include("proposals.urls")), + path("sponsors/", include("sponsors.urls")), path("admin/", admin.site.urls), path("__reload__/", include("django_browser_reload.urls")), path('i18n/', include('django.conf.urls.i18n')), ] +# Media and static files +urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) + urlpatterns += i18n_patterns( path("", include("website.urls")), path("accounts/", include("allauth.urls")), path("proposals/", include("proposals.urls")), + path("sponsors/", include("sponsors.urls")), path("admin/", admin.site.urls), ) \ No newline at end of file diff --git a/sponsors/__init__.py b/sponsors/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/sponsors/admin.py b/sponsors/admin.py new file mode 100644 index 0000000..76bae0b --- /dev/null +++ b/sponsors/admin.py @@ -0,0 +1,20 @@ +from django.contrib import admin + +from sponsors.models import Sponsor, SponsorshipPackage, File, TaggedFile + + +@admin.register(Sponsor) +class SponsorAdmin(admin.ModelAdmin): + pass + +@admin.register(SponsorshipPackage) +class SponsorshipPackageAdmin(admin.ModelAdmin): + pass + +@admin.register(File) +class FileAdmin(admin.ModelAdmin): + pass + +@admin.register(TaggedFile) +class TaggedFileAdmin(admin.ModelAdmin): + pass \ No newline at end of file diff --git a/sponsors/apps.py b/sponsors/apps.py new file mode 100644 index 0000000..c0183c4 --- /dev/null +++ b/sponsors/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class SponsorsConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'sponsors' diff --git a/sponsors/migrations/0001_initial.py b/sponsors/migrations/0001_initial.py new file mode 100644 index 0000000..a2ac1ca --- /dev/null +++ b/sponsors/migrations/0001_initial.py @@ -0,0 +1,54 @@ +# Generated by Django 4.2.16 on 2025-01-05 10:44 + +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='SponsorshipFile', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=255)), + ('description', models.TextField(blank=True, help_text='A Description of the file.')), + ('item', models.FileField(upload_to='sponsors_files')), + ], + ), + migrations.CreateModel( + name='SponsorshipPackage', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('order', models.IntegerField(default=1)), + ('name', models.CharField(help_text='The name of the sponsorship package.', max_length=255)), + ('number_available', models.IntegerField(null=True, validators=[django.core.validators.MinValueValidator(0)])), + ('currency', models.CharField(default='$', help_text='Currency symbol of the sponsorship package.', max_length=20)), + ('amount', models.DecimalField(decimal_places=2, help_text='The amount of the sponsorship package.', max_digits=12)), + ('short_description', models.TextField(help_text='A short description of the sponsorship package.')), + ('files', models.ManyToManyField(blank=True, help_text='The files of the sponsorship package.', related_name='packages', to='sponsors.sponsorshipfile')), + ], + options={ + 'ordering': ['order', '-amount', 'name'], + }, + ), + migrations.CreateModel( + name='Sponsor', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('order', models.IntegerField(default=1)), + ('name', models.CharField(max_length=255)), + ('description', models.TextField(help_text='A description of the sponsor.')), + ('url', models.URLField(blank=True, default='', help_text='The URL of the sponsor if needed.')), + ('packages', models.ManyToManyField(related_name='sponsors', to='sponsors.sponsorshippackage')), + ], + options={ + 'ordering': ['order', 'name', 'id'], + }, + ), + ] diff --git a/sponsors/migrations/0002_rename_sponsorshipfile_file_taggedfile.py b/sponsors/migrations/0002_rename_sponsorshipfile_file_taggedfile.py new file mode 100644 index 0000000..2cc6b93 --- /dev/null +++ b/sponsors/migrations/0002_rename_sponsorshipfile_file_taggedfile.py @@ -0,0 +1,27 @@ +# Generated by Django 4.2.16 on 2025-01-12 12:23 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('sponsors', '0001_initial'), + ] + + operations = [ + migrations.RenameModel( + old_name='SponsorshipFile', + new_name='File', + ), + migrations.CreateModel( + name='TaggedFile', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('tag_name', models.CharField(help_text='The name of the tag.', max_length=255)), + ('sponsor', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='files', to='sponsors.sponsor')), + ('tagged_file', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='sponsors.file')), + ], + ), + ] diff --git a/sponsors/migrations/0003_sponsor_hiring.py b/sponsors/migrations/0003_sponsor_hiring.py new file mode 100644 index 0000000..b1d4e17 --- /dev/null +++ b/sponsors/migrations/0003_sponsor_hiring.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.16 on 2025-01-17 15:05 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('sponsors', '0002_rename_sponsorshipfile_file_taggedfile'), + ] + + operations = [ + migrations.AddField( + model_name='sponsor', + name='hiring', + field=models.BooleanField(default=False, help_text='Whether the sponsor is hiring or not.'), + ), + ] diff --git a/sponsors/migrations/0004_sponsorshippackage_symbol.py b/sponsors/migrations/0004_sponsorshippackage_symbol.py new file mode 100644 index 0000000..d939b9d --- /dev/null +++ b/sponsors/migrations/0004_sponsorshippackage_symbol.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.16 on 2025-01-21 08:24 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('sponsors', '0003_sponsor_hiring'), + ] + + operations = [ + migrations.AddField( + model_name='sponsorshippackage', + name='symbol', + field=models.CharField(blank=True, help_text='The symbol of the sponsorship package.', max_length=1), + ), + ] diff --git a/sponsors/migrations/0005_alter_sponsorshippackage_symbol.py b/sponsors/migrations/0005_alter_sponsorshippackage_symbol.py new file mode 100644 index 0000000..1acfc22 --- /dev/null +++ b/sponsors/migrations/0005_alter_sponsorshippackage_symbol.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.16 on 2025-01-21 08:30 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('sponsors', '0004_sponsorshippackage_symbol'), + ] + + operations = [ + migrations.AlterField( + model_name='sponsorshippackage', + name='symbol', + field=models.CharField(blank=True, help_text='The symbol of the sponsorship package.', max_length=100), + ), + ] diff --git a/sponsors/migrations/__init__.py b/sponsors/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/sponsors/models.py b/sponsors/models.py new file mode 100644 index 0000000..4e81611 --- /dev/null +++ b/sponsors/models.py @@ -0,0 +1,116 @@ +from django.db import models +from django.core.validators import MinValueValidator +from django.utils.translation import gettext_lazy as _ + + +class File(models.Model): + """File for use in sponsor and sponsorship package description.""" + name = models.CharField(max_length=255) + description = models.TextField( + blank=True, + help_text=_("A Description of the file.") + ) + item = models.FileField( + upload_to='sponsors_files' + ) + + def __str__(self): + return u"%s (%s)" % (self.name, self.item.url) + + +class SponsorshipPackage(models.Model): + """A description of a sponsorship package.""" + order = models.IntegerField(default=1) + name = models.CharField( + max_length=255, + help_text=_("The name of the sponsorship package.") + ) + number_available = models.IntegerField( + null=True, + validators=[MinValueValidator(0)] + ) + currency = models.CharField( + max_length=20, + default="$", + help_text=_("Currency symbol of the sponsorship package.") + ) + amount = models.DecimalField( + max_digits=12, + decimal_places=2, + help_text=_("The amount of the sponsorship package.") + ) + short_description = models.TextField( + help_text=_("A short description of the sponsorship package.") + ) + files = models.ManyToManyField( + File, + related_name="packages", + blank=True, + help_text=_("The files of the sponsorship package.") + ) + symbol = models.CharField( + max_length=100, + blank=True, + help_text=_("The symbol of the sponsorship package.") + ) + + class Meta: + ordering = ["order", "-amount", "name"] + + def __str__(self): + return u"%s (amount: %.0f)" % (self.name, self.amount,) + + +class Sponsor(models.Model): + """A conference sponsor.""" + order = models.IntegerField(default=1) + name = models.CharField(max_length=255) + packages = models.ManyToManyField( + SponsorshipPackage, + related_name="sponsors", + ) + description = models.TextField( + help_text=_("A description of the sponsor.") + ) + url = models.URLField( + default="", + blank=True, + help_text=_("The URL of the sponsor if needed.") + ) + hiring = models.BooleanField( + default=False, + help_text=_("Whether the sponsor is hiring or not.") + ) + hiring_url = models.URLField( + default="", + blank=True, + help_text=_("Hiring URL of the sponsor.") + ) + + def __str__(self): + return u"%s" % (self.name,) + + class Meta: + ordering = ["order", "name", "id"] + + +class TaggedFile(models.Model): + """Tags for files associated with a given sponsor""" + tag_name = models.CharField( + max_length=255, + null=False, + help_text=_("The name of the tag.") + ) + tagged_file = models.ForeignKey( + File, + on_delete=models.CASCADE + ) + sponsor = models.ForeignKey( + Sponsor, + related_name="files", + on_delete=models.CASCADE + ) + + def __str__(self): + return u"%s - (%s)" % (self.sponsor.name, self.tag_name,) + diff --git a/sponsors/templates/sponsors/sponsors.html b/sponsors/templates/sponsors/sponsors.html new file mode 100644 index 0000000..dbdfec0 --- /dev/null +++ b/sponsors/templates/sponsors/sponsors.html @@ -0,0 +1,42 @@ +{% load sponsors %} +{% sponsors as all_sponsors %} +{% packages as all_packages %} + +

Here are the list of sponsors!

+ + + +

Here are the list of packages!

+ \ No newline at end of file diff --git a/sponsors/templatetags/__init__.py b/sponsors/templatetags/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/sponsors/templatetags/sponsors.py b/sponsors/templatetags/sponsors.py new file mode 100644 index 0000000..38af259 --- /dev/null +++ b/sponsors/templatetags/sponsors.py @@ -0,0 +1,27 @@ +from django import template + +from sponsors.models import Sponsor, SponsorshipPackage + +register = template.Library() + + + +@register.simple_tag +def sponsors(): + return { + "sponsors": Sponsor.objects.all().order_by('packages', 'order', 'id'), + } + + +@register.simple_tag +def packages(): + return { + "packages": SponsorshipPackage.objects.all().prefetch_related("files"), + } + +@register.simple_tag +def sponsor_tagged_image(sponsor, tag): + """return the corresponding url from the tagged image list.""" + if sponsor.files.filter(tag_name=tag).exists(): + return sponsor.files.filter(tag_name=tag).first().tagged_file.item.url + return '' diff --git a/sponsors/tests/__init__.py b/sponsors/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/sponsors/tests/test_models.py b/sponsors/tests/test_models.py new file mode 100644 index 0000000..3300a6c --- /dev/null +++ b/sponsors/tests/test_models.py @@ -0,0 +1,126 @@ +import pytest +import tempfile + +from typing import List, Tuple +from sponsors.models import Sponsor, SponsorshipPackage, File, TaggedFile + +from django.core.files.uploadedfile import SimpleUploadedFile + +def create_package(name: str, amount: float) -> SponsorshipPackage: + """Create a sponsor""" + package = SponsorshipPackage.objects.create( + name=name, + amount=amount + ) + return package + + +def create_sponsor(name: str, packages: List[Tuple[str, float]], hiring: bool = False, hiring_url: str = "") -> Sponsor: + """Create a sponsor with the given name and packages.""" + sponsor = Sponsor.objects.create(name=name, hiring=hiring, hiring_url=hiring_url) + for name, amount in packages: + package = create_package(name, amount) + sponsor.packages.add(package) + sponsor.save() + return sponsor + + +def create_file(name: str, desc: str) -> File: + """Create a File with a temporary image file.""" + temp_image = tempfile.NamedTemporaryFile(suffix=".png") + test_image = SimpleUploadedFile( + name="test_image.png", + content=open(temp_image.name, "rb").read(), + content_type="image/png" + ) + + return File.objects.create( + name=name, + description=desc, + item=test_image + ) + + +class TestSponsorshipModels: + + @pytest.mark.django_db + def test_sponsorship_package_creation(self): + """Create a sponsorship package.""" + package = create_package(u"Gold", 1000) + + assert package is not None + assert package.name == u"Gold" + assert package.amount == 1000 + + @pytest.mark.django_db + def test_sponsorship_with_single_package(self): + """Create sponsor with single sponsorship packages.""" + sponsor = create_sponsor(u"Django", [ + (u"Gold", 1000) + ], hiring=True, hiring_url="https://example.com") + + assert sponsor is not None + assert sponsor.name == u"Django" + assert sponsor.packages.count() == 1 + assert sponsor.hiring + assert sponsor.hiring_url == "https://example.com" + + package = sponsor.packages.first() + + assert package is not None + assert package.name == u"Gold" + assert package.amount == 1000 + + @pytest.mark.django_db + def test_sponsorship_with_multiple_packages(self): + """Creat sponsor with multiple sponsorship packages.""" + sponsor = create_sponsor(u"Django Org.", [ + (u"Gold", 1000), + (u"Sliver", 500), + ]) + + assert sponsor is not None + assert sponsor.name == u"Django Org." + assert sponsor.packages.count() == 2 + + packages = sponsor.packages.all() + + assert packages is not None + assert packages.count() == 2 + + +class TestFilesModels: + + @pytest.mark.django_db + def test_file_creation(self): + """Creates a file with name and description.""" + file = create_file(name="Awesome image", desc="This is a test file.") + + assert file is not None + assert file.name == u"Awesome image" + assert file.description == u"This is a test file." + assert file.item + + @pytest.mark.django_db + def test_tagged_file_creation(self): + """Create tagged file""" + file = create_file(name="Tagged Image", desc="This is a test file.") + sponsor = create_sponsor(u"Awesome Sponsor", [ + (u"Gold", 1000) + ]) + + assert file is not None + assert sponsor is not None + + tagged_file = TaggedFile.objects.create( + tag_name="logo", + tagged_file=file, + sponsor=sponsor + ) + + assert tagged_file is not None + assert tagged_file.tag_name == u"logo" + assert tagged_file.sponsor == sponsor + + + diff --git a/sponsors/urls.py b/sponsors/urls.py new file mode 100644 index 0000000..354e722 --- /dev/null +++ b/sponsors/urls.py @@ -0,0 +1,8 @@ +from django.urls import re_path + +from sponsors.views import SponsorsDetailView + +urlpatterns = [ + re_path(r"^$", SponsorsDetailView.as_view(), name="sponsors"), +] + diff --git a/sponsors/views.py b/sponsors/views.py new file mode 100644 index 0000000..5d14b9f --- /dev/null +++ b/sponsors/views.py @@ -0,0 +1,9 @@ +from django.views.generic import TemplateView +from sponsors.models import Sponsor + + + +class SponsorsDetailView(TemplateView): + template_name = "sponsors/sponsors.html" + model = Sponsor +