├── apps ├── __init__.py ├── blog │ ├── __init__.py │ ├── migrations │ │ ├── __init__.py │ │ ├── 0003_alter_post_slug.py │ │ ├── 0001_initial.py │ │ └── 0002_bibleverse.py │ ├── tests.py │ ├── apps.py │ ├── sitemaps.py │ ├── context_processors.py │ ├── templates │ │ └── blog │ │ │ ├── 403_csrf.html │ │ │ ├── 404.html │ │ │ ├── post_edit.html │ │ │ ├── post_new.html │ │ │ ├── detail.html │ │ │ ├── index.html │ │ │ └── about.html │ ├── forms.py │ ├── urls.py │ ├── tasks.py │ ├── admin.py │ ├── models.py │ ├── newsletter.py │ └── views.py ├── search │ ├── __init__.py │ ├── migrations │ │ └── __init__.py │ ├── models.py │ ├── tests.py │ ├── admin.py │ ├── urls.py │ ├── apps.py │ ├── views.py │ └── templates │ │ └── search │ │ └── search_result.html ├── sermon │ ├── __init__.py │ ├── migrations │ │ ├── __init__.py │ │ └── 0001_initial.py │ ├── templatetags │ │ ├── __init__.py │ │ └── sermon_tags.py │ ├── tests.py │ ├── apps.py │ ├── sitemaps.py │ ├── urls.py │ ├── admin.py │ ├── templates │ │ └── sermon │ │ │ ├── latest_sermons.html │ │ │ ├── sermon_pdf.html │ │ │ ├── detail.html │ │ │ └── index.html │ ├── models.py │ └── views.py ├── user │ ├── __init__.py │ ├── migrations │ │ ├── __init__.py │ │ └── 0001_initial.py │ ├── tests.py │ ├── admin.py │ ├── apps.py │ ├── templates │ │ ├── registration │ │ │ ├── password_change_done.html │ │ │ ├── password_reset_done.html │ │ │ ├── password_reset_complete.html │ │ │ ├── password_reset_confirm.html │ │ │ ├── password_reset_form.html │ │ │ ├── password_reset_email.html │ │ │ ├── password_change_form.html │ │ │ └── login.html │ │ └── user │ │ │ ├── register.html │ │ │ ├── profile_edit.html │ │ │ └── profile.html │ ├── signals.py │ ├── models.py │ ├── forms.py │ ├── urls.py │ └── views.py ├── utils │ ├── __init__.py │ ├── templatetags │ │ ├── __init__.py │ │ └── filters.py │ └── bible_books.py └── payment │ ├── __init__.py │ ├── migrations │ ├── __init__.py │ ├── 0002_donation_paid.py │ ├── 0003_donation_ref_id.py │ └── 0001_initial.py │ ├── tests.py │ ├── apps.py │ ├── context_processors.py │ ├── forms.py │ ├── templates │ └── payment │ │ ├── success.html │ │ ├── failure.html │ │ └── payment_form.html │ ├── admin.py │ ├── urls.py │ ├── models.py │ └── views.py ├── Procfile ├── static ├── search │ └── css │ │ └── search.css ├── blog │ ├── images │ │ ├── blog.jpg │ │ ├── logo.jpg │ │ ├── cross_1.png │ │ ├── search.png │ │ ├── latest_1.jpg │ │ ├── latest_2.jpg │ │ ├── latest_3.jpg │ │ ├── latest_4.jpg │ │ ├── logo_large.jpg │ │ ├── openmenu.png │ │ ├── quote_char.png │ │ └── sidebar_quote.jpg │ ├── js │ │ └── countdown.js │ └── css │ │ └── base.css ├── sermon │ ├── images │ │ ├── church_3.png │ │ ├── church_7.png │ │ ├── sermon.png │ │ ├── sermons.jpg │ │ ├── services.jpg │ │ ├── xsermon.webp │ │ ├── featured_1.jpg │ │ ├── featured_2.jpg │ │ ├── popular_1.jpg │ │ ├── sermon_big.jpg │ │ ├── services_1.png │ │ ├── services_2.png │ │ ├── services_3.png │ │ ├── services_4.png │ │ ├── services_5.png │ │ ├── services_6.png │ │ ├── sermon_image.jpg │ │ ├── sermon_pastor.jpg │ │ ├── sermon_single.jpg │ │ ├── sermon_small.jpg │ │ ├── sermon_4.svg │ │ ├── sermon_5.svg │ │ ├── sermon_3.svg │ │ ├── sermon_2.svg │ │ ├── sermon_6.svg │ │ └── sermon_1.svg │ ├── js │ │ └── read_text.js │ └── css │ │ └── sermon.css ├── user │ └── css │ │ └── form.css ├── payment │ └── js │ │ └── popup.js └── admin │ └── css │ └── base.css ├── config ├── __init__.py ├── settings │ ├── __init__.py │ ├── base.py │ ├── production.py │ └── local.py ├── asgi.py ├── wsgi.py ├── celery.py └── urls.py ├── .gitignore ├── .env.sample ├── templates ├── admin │ └── base_site.html └── base.html ├── Pipfile ├── manage.py └── README.md /apps/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/blog/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/search/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/sermon/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/user/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/utils/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/payment/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/blog/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/user/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/payment/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/search/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/sermon/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/sermon/templatetags/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/utils/templatetags/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: gunicorn config.wsgi --log-file - -------------------------------------------------------------------------------- /static/search/css/search.css: -------------------------------------------------------------------------------- 1 | .highlight { 2 | background-color: yellow; 3 | } -------------------------------------------------------------------------------- /apps/blog/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /apps/payment/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /apps/search/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | # Create your models here. 4 | -------------------------------------------------------------------------------- /apps/search/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /apps/sermon/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /apps/user/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /config/__init__.py: -------------------------------------------------------------------------------- 1 | from .celery import app as celery_app 2 | 3 | __all__ = (celery_app,) 4 | -------------------------------------------------------------------------------- /apps/search/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /static/blog/images/blog.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BOVAGE/church-management-system/HEAD/static/blog/images/blog.jpg -------------------------------------------------------------------------------- /static/blog/images/logo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BOVAGE/church-management-system/HEAD/static/blog/images/logo.jpg -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | staticfiles 3 | media 4 | 5 | 6 | # celery beat 7 | celerybeat* 8 | 9 | #redis 10 | dump.rdb -------------------------------------------------------------------------------- /static/blog/images/cross_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BOVAGE/church-management-system/HEAD/static/blog/images/cross_1.png -------------------------------------------------------------------------------- /static/blog/images/search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BOVAGE/church-management-system/HEAD/static/blog/images/search.png -------------------------------------------------------------------------------- /static/blog/images/latest_1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BOVAGE/church-management-system/HEAD/static/blog/images/latest_1.jpg -------------------------------------------------------------------------------- /static/blog/images/latest_2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BOVAGE/church-management-system/HEAD/static/blog/images/latest_2.jpg -------------------------------------------------------------------------------- /static/blog/images/latest_3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BOVAGE/church-management-system/HEAD/static/blog/images/latest_3.jpg -------------------------------------------------------------------------------- /static/blog/images/latest_4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BOVAGE/church-management-system/HEAD/static/blog/images/latest_4.jpg -------------------------------------------------------------------------------- /static/blog/images/logo_large.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BOVAGE/church-management-system/HEAD/static/blog/images/logo_large.jpg -------------------------------------------------------------------------------- /static/blog/images/openmenu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BOVAGE/church-management-system/HEAD/static/blog/images/openmenu.png -------------------------------------------------------------------------------- /static/blog/images/quote_char.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BOVAGE/church-management-system/HEAD/static/blog/images/quote_char.png -------------------------------------------------------------------------------- /static/sermon/images/church_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BOVAGE/church-management-system/HEAD/static/sermon/images/church_3.png -------------------------------------------------------------------------------- /static/sermon/images/church_7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BOVAGE/church-management-system/HEAD/static/sermon/images/church_7.png -------------------------------------------------------------------------------- /static/sermon/images/sermon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BOVAGE/church-management-system/HEAD/static/sermon/images/sermon.png -------------------------------------------------------------------------------- /static/sermon/images/sermons.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BOVAGE/church-management-system/HEAD/static/sermon/images/sermons.jpg -------------------------------------------------------------------------------- /static/sermon/images/services.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BOVAGE/church-management-system/HEAD/static/sermon/images/services.jpg -------------------------------------------------------------------------------- /static/sermon/images/xsermon.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BOVAGE/church-management-system/HEAD/static/sermon/images/xsermon.webp -------------------------------------------------------------------------------- /static/sermon/images/featured_1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BOVAGE/church-management-system/HEAD/static/sermon/images/featured_1.jpg -------------------------------------------------------------------------------- /static/sermon/images/featured_2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BOVAGE/church-management-system/HEAD/static/sermon/images/featured_2.jpg -------------------------------------------------------------------------------- /static/sermon/images/popular_1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BOVAGE/church-management-system/HEAD/static/sermon/images/popular_1.jpg -------------------------------------------------------------------------------- /static/sermon/images/sermon_big.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BOVAGE/church-management-system/HEAD/static/sermon/images/sermon_big.jpg -------------------------------------------------------------------------------- /static/sermon/images/services_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BOVAGE/church-management-system/HEAD/static/sermon/images/services_1.png -------------------------------------------------------------------------------- /static/sermon/images/services_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BOVAGE/church-management-system/HEAD/static/sermon/images/services_2.png -------------------------------------------------------------------------------- /static/sermon/images/services_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BOVAGE/church-management-system/HEAD/static/sermon/images/services_3.png -------------------------------------------------------------------------------- /static/sermon/images/services_4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BOVAGE/church-management-system/HEAD/static/sermon/images/services_4.png -------------------------------------------------------------------------------- /static/sermon/images/services_5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BOVAGE/church-management-system/HEAD/static/sermon/images/services_5.png -------------------------------------------------------------------------------- /static/sermon/images/services_6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BOVAGE/church-management-system/HEAD/static/sermon/images/services_6.png -------------------------------------------------------------------------------- /static/blog/images/sidebar_quote.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BOVAGE/church-management-system/HEAD/static/blog/images/sidebar_quote.jpg -------------------------------------------------------------------------------- /static/sermon/images/sermon_image.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BOVAGE/church-management-system/HEAD/static/sermon/images/sermon_image.jpg -------------------------------------------------------------------------------- /static/sermon/images/sermon_pastor.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BOVAGE/church-management-system/HEAD/static/sermon/images/sermon_pastor.jpg -------------------------------------------------------------------------------- /static/sermon/images/sermon_single.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BOVAGE/church-management-system/HEAD/static/sermon/images/sermon_single.jpg -------------------------------------------------------------------------------- /static/sermon/images/sermon_small.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BOVAGE/church-management-system/HEAD/static/sermon/images/sermon_small.jpg -------------------------------------------------------------------------------- /config/settings/__init__.py: -------------------------------------------------------------------------------- 1 | from .base import * 2 | 3 | if not config("DEBUG", cast=bool): 4 | from .production import * 5 | else: 6 | from .local import * 7 | -------------------------------------------------------------------------------- /apps/search/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | from .views import search 3 | 4 | app_name = "search" 5 | urlpatterns = [ 6 | path("", search, name="search"), 7 | ] 8 | -------------------------------------------------------------------------------- /apps/blog/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class BlogConfig(AppConfig): 5 | default_auto_field = "django.db.models.BigAutoField" 6 | name = "apps.blog" 7 | -------------------------------------------------------------------------------- /.env.sample: -------------------------------------------------------------------------------- 1 | SECRET_KEY= 2 | DEBUG= 3 | AWS_ACCESS_KEY_ID = 4 | AWS_SECRET_ACCESS_KEY = 5 | MAILCHIMP_KEY = 6 | MAILCHIMP_SERVER = 7 | DB_PASSWORD= 8 | PAYSTACK_SECRET= 9 | PAYSTACK_PUBLIC= -------------------------------------------------------------------------------- /apps/search/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class SearchConfig(AppConfig): 5 | default_auto_field = "django.db.models.BigAutoField" 6 | name = "apps.search" 7 | -------------------------------------------------------------------------------- /apps/sermon/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class SermonConfig(AppConfig): 5 | default_auto_field = "django.db.models.BigAutoField" 6 | name = "apps.sermon" 7 | -------------------------------------------------------------------------------- /apps/payment/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class PaymentConfig(AppConfig): 5 | default_auto_field = "django.db.models.BigAutoField" 6 | name = "apps.payment" 7 | -------------------------------------------------------------------------------- /apps/payment/context_processors.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | 3 | 4 | def get_payment_secret(request): 5 | context = {"public_key": settings.PAYSTACK_PUBLIC} 6 | return context 7 | -------------------------------------------------------------------------------- /config/asgi.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from django.core.asgi import get_asgi_application 4 | 5 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings") 6 | 7 | application = get_asgi_application() 8 | -------------------------------------------------------------------------------- /config/wsgi.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from django.core.wsgi import get_wsgi_application 4 | 5 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings") 6 | 7 | application = get_wsgi_application() 8 | -------------------------------------------------------------------------------- /apps/utils/templatetags/filters.py: -------------------------------------------------------------------------------- 1 | from django import template 2 | 3 | register = template.Library() 4 | 5 | 6 | @register.filter(name="pretty_") 7 | def underscore_to_space(value): 8 | return value.replace("_", " ") 9 | -------------------------------------------------------------------------------- /apps/payment/forms.py: -------------------------------------------------------------------------------- 1 | from django.forms import ModelForm 2 | from .models import Donation 3 | 4 | 5 | class PaymentForm(ModelForm): 6 | class Meta: 7 | model = Donation 8 | exclude = ["date", "paid", "ref_id"] 9 | -------------------------------------------------------------------------------- /apps/payment/templates/payment/success.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% load static %} 3 | {% block content %} 4 |
5 |

Payment Successful.Thanks for Donating. God Bless You

6 |
7 | {% endblock %} -------------------------------------------------------------------------------- /apps/user/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from .models import Profile 3 | 4 | # Register your models here. 5 | @admin.register(Profile) 6 | class ProfileAdmin(admin.ModelAdmin): 7 | list_display = ["user", "social_link", "pic"] 8 | -------------------------------------------------------------------------------- /apps/user/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class UserConfig(AppConfig): 5 | default_auto_field = "django.db.models.BigAutoField" 6 | name = "apps.user" 7 | 8 | def ready(self): 9 | from . import signals 10 | -------------------------------------------------------------------------------- /apps/payment/templates/payment/failure.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% load static %} 3 | {% block content %} 4 |
5 |

Payment not Successful. Please Try again

6 |
7 | {% endblock %} -------------------------------------------------------------------------------- /apps/sermon/sitemaps.py: -------------------------------------------------------------------------------- 1 | from django.contrib.sitemaps import Sitemap 2 | from .models import Sermon 3 | 4 | 5 | class SermonSitemap(Sitemap): 6 | changefreq = "daily" 7 | priority = 1.0 8 | 9 | def items(self): 10 | return Sermon.objects.all() 11 | -------------------------------------------------------------------------------- /config/celery.py: -------------------------------------------------------------------------------- 1 | import os 2 | from celery import Celery 3 | 4 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings") 5 | 6 | app = Celery("config") 7 | 8 | app.config_from_object("django.conf:settings", namespace="CELERY") 9 | app.autodiscover_tasks() 10 | -------------------------------------------------------------------------------- /apps/blog/sitemaps.py: -------------------------------------------------------------------------------- 1 | from django.contrib.sitemaps import Sitemap 2 | from .models import Post 3 | 4 | 5 | class PostSitemap(Sitemap): 6 | changefreq = "weekly" 7 | priority = 0.9 8 | 9 | def items(self): 10 | return Post.objects.all() 11 | 12 | def lastmod(self, obj): 13 | return obj.last_modified 14 | -------------------------------------------------------------------------------- /apps/sermon/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | from .views import index, detail, sermon_pdf 3 | 4 | app_name = "sermon" 5 | urlpatterns = [ 6 | path("", index, name="sermons"), 7 | path("////", detail, name="sermon_single"), 8 | path("/pdf", sermon_pdf, name="sermon_pdf"), 9 | ] 10 | -------------------------------------------------------------------------------- /apps/blog/context_processors.py: -------------------------------------------------------------------------------- 1 | from apps.blog.models import Announcement 2 | 3 | 4 | def announcement(request): 5 | # only show one announcement at a time 6 | try: 7 | context = {"announcement": Announcement.objects.filter(featured=True)[0]} 8 | except IndexError: 9 | context = {"announcement": ""} 10 | return context 11 | -------------------------------------------------------------------------------- /apps/payment/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from .models import Donation 3 | 4 | # Register your models here. 5 | class DonationAdmin(admin.ModelAdmin): 6 | list_display = ["full_name", "amount", "email_address", "date", "paid", "ref_id"] 7 | list_filter = ["full_name", "paid", "date"] 8 | 9 | 10 | admin.site.register(Donation, DonationAdmin) 11 | -------------------------------------------------------------------------------- /apps/sermon/templatetags/sermon_tags.py: -------------------------------------------------------------------------------- 1 | from atexit import register 2 | from django import template 3 | from ..models import Sermon 4 | 5 | register = template.Library() 6 | 7 | 8 | @register.inclusion_tag("sermon/latest_sermons.html") 9 | def show_latest_sermons(count=4): 10 | latest_sermons = Sermon.objects.all()[:count] 11 | return {"latest_sermons": latest_sermons} 12 | -------------------------------------------------------------------------------- /apps/payment/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | from .views import transact, verfiy_payment, success, failure 3 | 4 | app_name = "payment" 5 | urlpatterns = [ 6 | path("", transact, name="donate"), 7 | path("verify//", verfiy_payment, name="verify"), 8 | path("success/", success, name="sucess"), 9 | path("failure/", failure, name="failure"), 10 | ] 11 | -------------------------------------------------------------------------------- /apps/sermon/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from .models import Sermon 3 | 4 | # Register your models here. 5 | class SermonAdmin(admin.ModelAdmin): 6 | list_display = ["title", "author", "summary", "date_created"] 7 | list_filter = ["title", "author", "tags"] 8 | prepopulated_fields = {"slug": ("title",)} 9 | 10 | 11 | admin.site.register(Sermon, SermonAdmin) 12 | -------------------------------------------------------------------------------- /apps/blog/migrations/0003_alter_post_slug.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.0 on 2022-05-23 07:40 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("blog", "0002_bibleverse"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name="post", 15 | name="slug", 16 | field=models.SlugField(blank=True), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /apps/payment/migrations/0002_donation_paid.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.0 on 2022-05-19 22:31 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("payment", "0001_initial"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name="donation", 15 | name="paid", 16 | field=models.BooleanField(default=False), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /templates/admin/base_site.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/base.html" %} 2 | {% load static %} 3 | 4 | {% block title %}{% if subtitle %}{{ subtitle }} | {% endif %}{{ title }} | {{ site_title|default:_('Django site admin') }}{% endblock %} 5 | 6 | {% block branding %} 7 |

{{ site_header|default:_('Django administration') }}church logo

8 | {% endblock %} 9 | 10 | {% block nav-global %}{% endblock %} 11 | -------------------------------------------------------------------------------- /apps/sermon/templates/sermon/latest_sermons.html: -------------------------------------------------------------------------------- 1 | {% load static %} 2 | {% for sermon in latest_sermons %} 3 |
4 | pray 5 | 6 |
7 | 8 |

{{ sermon.title }}

9 |
10 |
11 | {% empty %} 12 |

No Sermon to show

13 | {% endfor %} -------------------------------------------------------------------------------- /apps/user/templates/registration/password_change_done.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% load static %} 3 | {% block title %}Believe - Password Change Done{% endblock title %} 4 | {% block current-page %}Password Rest Change Done{% endblock current-page %} 5 | {% block content %} 6 |
7 |
8 |

Password changed successful

9 |

Your password was changed.

10 |
11 |
12 | {% endblock content %} -------------------------------------------------------------------------------- /apps/user/templates/registration/password_reset_done.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% load static %} 3 | {% block title %}Believe - Email Sent{% endblock title %} 4 | {% block current-page %}Email Sent{% endblock current-page %} 5 | {% block content %} 6 |
7 |
8 |

Check your inbox.

9 |

We've emailed you instructions for setting your password. You should receive the email shortly!

10 |
11 |
12 | {% endblock content %} -------------------------------------------------------------------------------- /apps/blog/templates/blog/403_csrf.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% load static %} 3 | {% block title %}Believe - Error 403{% endblock title %} 4 | {% block current-page %}Error 403{% endblock current-page %} 5 | {% block content %} 6 |
7 |
8 |
9 | 10 |
11 |
12 |
13 | {% endblock content %} -------------------------------------------------------------------------------- /apps/payment/migrations/0003_donation_ref_id.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.0 on 2022-05-19 22:38 2 | 3 | from django.db import migrations, models 4 | import uuid 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ("payment", "0002_donation_paid"), 11 | ] 12 | 13 | operations = [ 14 | migrations.AddField( 15 | model_name="donation", 16 | name="ref_id", 17 | field=models.UUIDField(default=uuid.uuid4, editable=False, unique=True), 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /apps/user/signals.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth import get_user_model 2 | from django.db.models.signals import post_save 3 | from django.dispatch import receiver 4 | from .models import Profile 5 | 6 | User = get_user_model() 7 | 8 | 9 | @receiver(post_save, sender=User) 10 | def create_user_profile(sender, instance, created, **kwargs): 11 | if created: 12 | Profile.objects.create(user=instance) 13 | 14 | 15 | @receiver(post_save, sender=User) 16 | def save_user_profile(sender, instance, **kwargs): 17 | instance.profile.save() 18 | -------------------------------------------------------------------------------- /apps/blog/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | from .models import Category, Post, Comment 3 | 4 | 5 | class CommentForm(forms.ModelForm): 6 | class Meta: 7 | model = Comment 8 | fields = ["comment_text"] 9 | widgets = { 10 | "comment_text": forms.Textarea( 11 | attrs={"placeholder": "Write your comment here.."} 12 | ), 13 | } 14 | 15 | 16 | class PostForm(forms.ModelForm): 17 | class Meta: 18 | model = Post 19 | fields = ["title", "slug", "category", "post_image", "body", "description"] 20 | -------------------------------------------------------------------------------- /apps/user/templates/user/register.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% load static %} 3 | {% block title %}Believe - Register{% endblock title %} 4 | {% block current-page %}Register{% endblock current-page %} 5 | {% block content %} 6 |
7 |
8 | {% csrf_token %} 9 | {{ user_form.as_p }} 10 | {{ profile_form.as_p }} 11 | 12 |
13 |
14 | {% endblock content %} -------------------------------------------------------------------------------- /apps/payment/models.py: -------------------------------------------------------------------------------- 1 | from webbrowser import get 2 | from django.db import models 3 | import uuid 4 | 5 | # Create your models here. 6 | class Donation(models.Model): 7 | full_name = models.CharField(max_length=35) 8 | email_address = models.EmailField() 9 | amount = models.DecimalField(decimal_places=2, max_digits=7) 10 | date = models.DateTimeField() 11 | ref_id = models.UUIDField(default=uuid.uuid4, editable=False, unique=True) 12 | paid = models.BooleanField(default=False) 13 | 14 | def __str__(self): 15 | return f"{self.full_name} paid {self.amount}" 16 | -------------------------------------------------------------------------------- /apps/payment/templates/payment/payment_form.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% load static %} 3 | {% block content %} 4 |
5 |
6 | {% csrf_token %} 7 | {{ form.as_p }} 8 | 9 |
10 |
11 | {% endblock content %} 12 | 13 | {% block js %} 14 | {{ public_key|json_script:"public_key" }} 15 | 16 | 17 | {% endblock js %} -------------------------------------------------------------------------------- /apps/user/templates/registration/password_reset_complete.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% load static %} 3 | {% block title %}Believe - Password Reset Complete{% endblock title %} 4 | {% block current-page %}Password Rest Complete{% endblock current-page %} 5 | {% block content %} 6 |
7 |
8 |

Password reset complete

9 |

Your new password has been set. You can log in now on the 10 | log in page. 11 |

12 |
13 |
14 | {% endblock content %} -------------------------------------------------------------------------------- /static/user/css/form.css: -------------------------------------------------------------------------------- 1 | form.form input { 2 | display: block; 3 | width: 100%; 4 | } 5 | 6 | textarea { 7 | width: 100%; 8 | } 9 | 10 | form.form { 11 | width: 300px; 12 | margin: 0 auto 100px; 13 | border: 2px solid #bfbfbf; 14 | border-radius: 8px; 15 | padding: 45px 25px; 16 | box-shadow: 1px 1px 5px 2px rgba(0,0,0,.175), 17 | -1px -1px 5px 2px rgba(0,0,0,.175); 18 | } 19 | 20 | .blue-link { 21 | color: blue; 22 | text-decoration: underline; 23 | } 24 | 25 | @media screen and (max-width: 580px) { 26 | form.form { 27 | width: 100%; 28 | } 29 | } -------------------------------------------------------------------------------- /apps/user/templates/registration/password_reset_confirm.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% load static %} 3 | {% block title %}Believe - Password Reset{% endblock title %} 4 | {% block current-page %}Password Reset{% endblock current-page %} 5 | {% block content %} 6 |
7 |
8 |

Set a new password!

9 |
{% csrf_token %} 10 | {{ form.as_p }} 11 | 12 |
13 |
14 |
15 | {% endblock content %} -------------------------------------------------------------------------------- /apps/user/templates/registration/password_reset_form.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block title %}Forgot your Password{% endblock title %} 4 | 5 | {% block content %} 6 |
7 |
8 |

Forgot your password?

9 |

Enter your email address below, and we'll email instructions for setting a new one.

10 |
11 | {% csrf_token %} 12 | {{ form.as_p }} 13 | 14 |
15 |
16 |
17 | {% endblock content %} -------------------------------------------------------------------------------- /apps/blog/templates/blog/404.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% load static %} 3 | {% block title %}Error 404{% endblock title %} 4 | {% block current-page %}Error 404{% endblock current-page %} 5 | {% block content %} 6 |
7 |
8 |
9 | 10 |
11 |
12 |
13 | {% endblock content %} -------------------------------------------------------------------------------- /static/sermon/js/read_text.js: -------------------------------------------------------------------------------- 1 | const playBtn = document.getElementById('play'); 2 | const content = document.querySelector('.content'); 3 | 4 | function text_to_speech(text, startOver=true){ 5 | if (startOver) { 6 | speechSynthesis.cancel(); 7 | } 8 | let utterance = new SpeechSynthesisUtterance(text); 9 | utterance.voice = speechSynthesis.getVoices()[0]; 10 | speechSynthesis.speak(utterance); 11 | } 12 | 13 | playBtn.addEventListener('click', text_to_speech.bind(null, content.textContent)); 14 | 15 | // stops the sound once on page reload 16 | window.addEventListener('beforeunload',() => speechSynthesis.cancel(), false); 17 | 18 | 19 | -------------------------------------------------------------------------------- /apps/user/models.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.db import models 3 | from django.urls import reverse 4 | 5 | 6 | class Profile(models.Model): 7 | user = models.OneToOneField(settings.AUTH_USER_MODEL, on_delete=models.CASCADE) 8 | bio = models.TextField(max_length=500, blank=True) 9 | role = models.CharField(max_length=30, blank=True) 10 | pic = models.ImageField(blank=True, default="media/avatar.png", upload_to="media/") 11 | social_link = models.URLField() 12 | 13 | def __str__(self) -> str: 14 | return self.user.username 15 | 16 | def get_absolute_url(self): 17 | return reverse("user:profile", args=(self.id,)) 18 | -------------------------------------------------------------------------------- /apps/user/templates/registration/password_reset_email.html: -------------------------------------------------------------------------------- 1 | {% load i18n %}{% autoescape off %} 2 | {% blocktranslate %}You're receiving this email because you requested a password reset for your user account at {{ site_name }}.{% endblocktranslate %} 3 | 4 | {% translate "Please go to the following page and choose a new password:" %} 5 | {% block reset_link %} 6 | {{ protocol }}://{{ domain }}{% url 'user:password_reset_confirm' uidb64=uid token=token %} 7 | {% endblock %} 8 | {% translate 'Your username, in case you’ve forgotten:' %} {{ user.get_username }} 9 | 10 | {% translate "Thanks for using our site!" %} 11 | 12 | {% blocktranslate %}The {{ site_name }} team{% endblocktranslate %} 13 | 14 | {% endautoescape %} 15 | -------------------------------------------------------------------------------- /apps/blog/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | from .views import ( 3 | edit_post, 4 | index, 5 | detail, 6 | about, 7 | create_post, 8 | delete_post, 9 | subscribe, 10 | unsubscribe, 11 | ) 12 | 13 | app_name = "blog" 14 | urlpatterns = [ 15 | path("", index, name="index"), 16 | path("post/new/", create_post, name="new"), 17 | path("post///", detail, name="detail"), 18 | path("post///delete", delete_post, name="delete"), 19 | path("post///edit", edit_post, name="edit"), 20 | path("subscribe", subscribe, name="subscribe"), 21 | path("unsubscribe/", unsubscribe, name="unsubscribe"), 22 | path("about/", about, name="about"), 23 | ] 24 | -------------------------------------------------------------------------------- /apps/user/templates/user/profile_edit.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% load static %} 3 | {% block title %}Believe - Register{% endblock title %} 4 | {% block current-page %}Register{% endblock current-page %} 5 | {% block style %}{% endblock style %} 6 | {% block content %} 7 |
8 |
9 |
10 | {% csrf_token %} 11 | {{ user_form.as_p }} 12 | {{ profile_form.as_p }} 13 | 14 |
15 |
16 |
17 | {% endblock content %} -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | url = "https://pypi.org/simple" 3 | verify_ssl = true 4 | name = "pypi" 5 | 6 | [packages] 7 | asgiref = "==3.4.1" 8 | boto3 = "==1.20.21" 9 | botocore = "==1.23.21" 10 | dj-database-url = "==0.5.0" 11 | django = "==4.0" 12 | django-ckeditor = "==6.2.0" 13 | django-js-asset = "==1.2.2" 14 | django-storages = "==1.12.3" 15 | gunicorn = "==20.1.0" 16 | jmespath = "==0.10.0" 17 | pillow = "==8.4.0" 18 | psycopg2 = "==2.9.2" 19 | python-dateutil = "==2.8.2" 20 | python-decouple = "==3.5" 21 | pytz = "==2021.3" 22 | s3transfer = "==0.5.0" 23 | six = "==1.16.0" 24 | sqlparse = "==0.4.2" 25 | tzdata = "==2021.5" 26 | urllib3 = "==1.26.7" 27 | whitenoise = "==5.3.0" 28 | mailchimp-marketing = "*" 29 | django-taggit = "*" 30 | redis = "*" 31 | celery = "==4.4.2" 32 | django-celery-beat = "==2.0.0" 33 | flower = "==0.9.5" 34 | weasyprint = "*" 35 | 36 | [dev-packages] 37 | black = "*" 38 | 39 | [requires] 40 | python_version = "3.9" 41 | -------------------------------------------------------------------------------- /apps/blog/tasks.py: -------------------------------------------------------------------------------- 1 | from celery import shared_task 2 | from celery.schedules import crontab 3 | from config.celery import app 4 | from .models import BibleVerse 5 | import redis 6 | from django.conf import settings 7 | 8 | # connect to redis 9 | r = redis.Redis( 10 | host=settings.REDIS_HOST, port=settings.REDIS_PORT, db=settings.REDIS_DB 11 | ) 12 | 13 | 14 | @shared_task 15 | def today_bible_verse(id): 16 | today_bible_verse = BibleVerse.objects.get(id=id) 17 | today_bible_verse = today_bible_verse.get_bible_verse() 18 | # store in redis 19 | r.set("today_bible_verse", today_bible_verse) 20 | return today_bible_verse 21 | 22 | 23 | app.conf.beat_schedule = { 24 | # Executes at the midnight of every day 25 | "get-daily-bible-verse": { 26 | "task": "blog.tasks.today_bible_verse", 27 | "schedule": crontab(hour=0, minute=5), 28 | "args": 1, # (BibleVerse.today.first().id,), 29 | }, 30 | } 31 | -------------------------------------------------------------------------------- /apps/blog/templates/blog/post_edit.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% load static %} 3 | {% block title %}Believe - Edit Post{% endblock title %} 4 | {% block current-page %}Post/Edit{% endblock current-page %} 5 | {% block content %} 6 |
7 |
8 | {% csrf_token %} 9 | {{ form.media }} 10 | {{ form.as_p }} 11 | 12 |
13 |
14 | {% endblock content %} 15 | 16 | {% block js %} 17 | 18 | 19 | 27 | {% endblock js %} -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Django's command-line utility for administrative tasks.""" 3 | import os 4 | import sys 5 | from pathlib import Path 6 | 7 | 8 | def main(): 9 | """Run administrative tasks.""" 10 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings") 11 | try: 12 | from django.core.management import execute_from_command_line 13 | except ImportError as exc: 14 | raise ImportError( 15 | "Couldn't import Django. Are you sure it's installed and " 16 | "available on your PYTHONPATH environment variable? Did you " 17 | "forget to activate a virtual environment?" 18 | ) from exc 19 | # This allows easy placement of apps within the interior apps directory. 20 | sys.path.append(f'{Path(__file__).parent / "apps"}') 21 | sys.path.append(f'{Path(__file__).parent / "config"}') 22 | execute_from_command_line(sys.argv) 23 | 24 | 25 | if __name__ == "__main__": 26 | main() 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # church-management-system 2 | This is a church management website created with Django. 3 | 4 | # Features 5 | - [x] Authentication & Permission 6 | - [x] fully functional blog section 7 | - [x] Rich Text Editor for writing blog post 8 | - [x] Profile for members 9 | - [x] Announcement of events 10 | - [x] countdown of the earliest event 11 | - [x] custom error pages 12 | - [x] image upload 13 | - [x] One Time notification 14 | - [x] newsletter 15 | - [x] Sermon 16 | - [x] daily bible verse on home page 17 | - [x] generate pdf file of sermon when file icon is clicked 18 | - [x] Audio Sermon 19 | - [x] search 20 | - [x] customized admin dashboard 21 | - [x] integrating [Paystack](https://paystack.com/) for donations 22 | - [x] Simple Recommender System 23 | - [x] Simple Ranking System 24 | 25 | # Database 26 | Postgresql 27 | 28 | # Technologies Used 29 | - AWS S3 for serving static files & file upload 30 | - Redis for Caching, Message Broker and Leader board 31 | 32 | # Video Demo 33 | 34 | -------------------------------------------------------------------------------- /apps/payment/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.0 on 2022-05-19 22:22 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | initial = True 9 | 10 | dependencies = [] 11 | 12 | operations = [ 13 | migrations.CreateModel( 14 | name="Donation", 15 | fields=[ 16 | ( 17 | "id", 18 | models.BigAutoField( 19 | auto_created=True, 20 | primary_key=True, 21 | serialize=False, 22 | verbose_name="ID", 23 | ), 24 | ), 25 | ("full_name", models.CharField(max_length=35)), 26 | ("email_address", models.EmailField(max_length=254)), 27 | ("amount", models.DecimalField(decimal_places=2, max_digits=7)), 28 | ("date", models.DateTimeField()), 29 | ], 30 | ), 31 | ] 32 | -------------------------------------------------------------------------------- /apps/blog/templates/blog/post_new.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% load static %} 3 | {% block title %}Believe - Create Post{% endblock title %} 4 | {% block current-page %}Post/New{% endblock current-page %} 5 | {% block content %} 6 |
7 |
8 |
9 | {% csrf_token %} 10 | {{ form.media }} 11 | {{ form.as_p }} 12 | 13 |
14 |
15 |
16 | {% endblock content %} 17 | 18 | {% block js %} 19 | 20 | 21 | 29 | {% endblock js %} -------------------------------------------------------------------------------- /apps/user/templates/user/profile.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% load static %} 3 | {% block title %}Believe - Profile{% endblock title %} 4 | {% block current-page %}Profile{% endblock current-page %} 5 | {% block content %} 6 |
7 | 8 |
9 | {% if request.user == user %} 10 | Edit 11 | Change Password 12 | {% endif %} 13 | profile image 14 |

Username: {{ user.username }}

15 |

Firstname: {{ user.first_name }}

16 |

Lastname: {{ user.last_name }}

17 |

Email Address: {{ user.email }}

18 |

Social Link: {{ profile.social_link }}

19 |

Bio: {{ profile.bio }}

20 |

Role: {{ profile.role }}

21 |
22 |
23 | {% endblock content %} -------------------------------------------------------------------------------- /apps/blog/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from apps.blog.models import Announcement, Category, Comment, Post, BibleVerse 3 | 4 | # Register your models here. 5 | class CommentInline(admin.TabularInline): 6 | model = Comment 7 | 8 | 9 | class PostAdmin(admin.ModelAdmin): 10 | list_display = ["title", "author", "date_created"] 11 | list_filter = ["title", "author", "date_created"] 12 | search_fields = ["title", "body", "description"] 13 | prepopulated_fields = {"slug": ("title",)} 14 | inlines = [CommentInline] 15 | 16 | 17 | class CommentAdmin(admin.ModelAdmin): 18 | list_display = ["user", "comment_text", "post"] 19 | search_fields = ["user__username", "comment_text", "post__title"] 20 | list_filter = ["user", "post"] 21 | 22 | 23 | class BibleVerseAdmin(admin.ModelAdmin): 24 | list_display = ["bible_verse", "ref", "date_for"] 25 | 26 | 27 | admin.site.register(Post, PostAdmin) 28 | admin.site.register(Comment, CommentAdmin) 29 | admin.site.register(Category) 30 | admin.site.register(Announcement) 31 | admin.site.register(BibleVerse, BibleVerseAdmin) 32 | -------------------------------------------------------------------------------- /apps/user/templates/registration/password_change_form.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% load static %} 3 | {% block title %}Believe - Password Change{% endblock title %} 4 | {% block current-page %}Password Change{% endblock current-page %} 5 | {% block content %} 6 |
7 |
8 |
9 |

Password Change

10 |

Please enter your old password, for security's sake, and then enter your 11 | new password twice so we can verify you typed it in correctly. 12 |

13 |
{% csrf_token %} 14 | {{ form.non_field_errors }} 15 | {% for field in form %} 16 |
17 | {{ field.label_tag }} {{ field }} 18 | {{ field.errors }} 19 |
20 | {% endfor %} 21 | 22 |
23 |
24 |
25 | 26 |
27 | {% endblock content %} -------------------------------------------------------------------------------- /apps/search/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render 2 | from apps.blog.models import Post 3 | from apps.sermon.models import Sermon 4 | from django.contrib.postgres.search import SearchVector, SearchQuery, SearchRank 5 | from itertools import chain 6 | from django.contrib.auth import get_user_model 7 | 8 | # Create your views here. 9 | def search(request): 10 | query = request.GET.get("q") 11 | search_query = SearchQuery(query) 12 | post_search_vector = SearchVector("title", "body") 13 | sermon_search_vector = SearchVector("title", "content") 14 | post_search = ( 15 | Post.objects.annotate( 16 | search=post_search_vector, rank=SearchRank(post_search_vector, search_query) 17 | ) 18 | .filter(search=search_query) 19 | .order_by("-rank") 20 | ) 21 | sermon_search = ( 22 | Sermon.objects.annotate( 23 | search=sermon_search_vector, 24 | rank=SearchRank(sermon_search_vector, search_query), 25 | ) 26 | .filter(search=search_query) 27 | .order_by("-rank") 28 | ) 29 | r = chain(post_search, sermon_search) 30 | result = list(r) 31 | context = {"search_result": result, "query": query} 32 | return render(request, "search/search_result.html", context) 33 | -------------------------------------------------------------------------------- /static/sermon/css/sermon.css: -------------------------------------------------------------------------------- 1 | img { 2 | max-width: 100%; 3 | } 4 | 5 | .two-equ-cols-container { 6 | display: grid; 7 | grid-template-columns: repeat(2, 1fr); 8 | gap: 40px 15px; 9 | } 10 | 11 | .heading-subtitle { 12 | font-family: 'Bilbo',serif; 13 | font-size: 1rem; 14 | margin-bottom: 30px; 15 | } 16 | 17 | .sermon-wrapper { 18 | padding: 100px 0; 19 | } 20 | 21 | #today-sermon-wrapper { 22 | background-color: #f0f4f8; 23 | background-image:url('../images/sermon.png'); 24 | background-repeat: no-repeat; 25 | background-size: cover; 26 | background-position: center center; 27 | border-bottom: 3px solid var(--red-text-bg); 28 | } 29 | 30 | .today-sermon-container { 31 | margin: 0 var(--main-margin) 25px; 32 | } 33 | 34 | .sermon-details { 35 | display: flex; 36 | flex-flow: column nowrap; 37 | justify-content: space-between; 38 | padding-left: 10px; 39 | } 40 | 41 | .sermon-actions { 42 | display: flex; 43 | flex-flow: row nowrap; 44 | align-items: flex-end; 45 | } 46 | 47 | .sermon-actions img { 48 | width: 30px; 49 | height: 30px; 50 | margin-right: 20px; 51 | } 52 | 53 | 54 | #play { 55 | cursor: pointer; 56 | } 57 | /* overidding base.css */ 58 | .showcase-container { 59 | margin-bottom: 0; 60 | } 61 | -------------------------------------------------------------------------------- /static/blog/js/countdown.js: -------------------------------------------------------------------------------- 1 | const DAYS = document.getElementById("days"); 2 | const HOURS = document.getElementById("hours"); 3 | const MINS = document.getElementById("mins"); 4 | const SECS = document.getElementById("secs"); 5 | const datedetail = document.getElementById("countdown-date"); 6 | 7 | // let date = "25 Dec 2021"; 8 | let date = datedetail.textContent; 9 | 10 | function formatTime(time) { 11 | return time < 10? `0${time}`: time; 12 | } 13 | 14 | function countdown() { 15 | countdownDate = new Date(date); 16 | if (new Date() >= countdownDate) { 17 | console.log('Countdown Completed') 18 | return ; 19 | } 20 | let totalSeconds = (countdownDate - new Date()) / 1000; 21 | let days = Math.floor(totalSeconds / 3600 / 24); 22 | let hours = Math.floor(totalSeconds / 3600) % 24; 23 | let minutes = Math.floor(totalSeconds / 60) % 60; 24 | let seconds = Math.floor(totalSeconds) % 60; 25 | DAYS.innerHTML = days + ` Days`; 26 | HOURS.innerHTML = formatTime(hours) + ` Hours`; 27 | MINS.innerHTML = formatTime(minutes) + ` Minutes`; 28 | SECS.innerHTML = formatTime(seconds) + ` Seconds`; 29 | setTimeout(() => { 30 | countdown(); 31 | }, 1000); 32 | } 33 | 34 | if (date != false) { 35 | countdown(); 36 | } -------------------------------------------------------------------------------- /config/urls.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.urls import path, include 3 | from django.conf import settings 4 | from django.conf.urls.static import static 5 | from django.conf.urls import handler404, handler403 6 | from django.contrib.sitemaps.views import sitemap 7 | from apps.sermon.sitemaps import SermonSitemap 8 | from apps.blog.sitemaps import PostSitemap 9 | 10 | 11 | sitemaps = { 12 | "posts": PostSitemap, 13 | "sermons": SermonSitemap, 14 | } 15 | 16 | urlpatterns = [ 17 | path("admin/", admin.site.urls), 18 | path("", include("apps.blog.urls")), 19 | path("user/", include("apps.user.urls")), 20 | path("sermon/", include("apps.sermon.urls")), 21 | path("search/", include("apps.search.urls")), 22 | path("payment/", include("apps.payment.urls")), 23 | path( 24 | "sitemap.xml", 25 | sitemap, 26 | {"sitemaps": sitemaps}, 27 | name="django.contrib.sitemaps.views.sitemap", 28 | ), 29 | ] 30 | handler404 = "apps.blog.views.error_404" 31 | handler403 = "apps.blog.views.error_403" 32 | 33 | if settings.DEBUG: 34 | urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) 35 | 36 | # admin site overrides 37 | admin.site.index_title = "Believe Administration" 38 | admin.site.site_header = "Believe Administration" 39 | admin.site.site_title = "Believe Admin" 40 | -------------------------------------------------------------------------------- /static/sermon/images/sermon_4.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /apps/user/templates/registration/login.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% load static %} 3 | {% block title %}Believe - Login{% endblock title %} 4 | {% block style %}{% endblock style %} 5 | {% block current-page %}Login{% endblock current-page %} 6 | {% block content %} 7 |
8 |
9 |
10 | {% csrf_token %} 11 | {% for err in form.non_field_errors %} 12 |

{{ err }}

13 | {% endfor %} 14 | 15 | {{ form.username }} 16 | 17 | {{ form.password }} 18 | 19 | 20 |

Not a member?Register

21 |

Forgot Password

22 |
23 |
24 |
25 | {% endblock content %} -------------------------------------------------------------------------------- /apps/utils/bible_books.py: -------------------------------------------------------------------------------- 1 | books_of_bible = [ 2 | "Genesis", 3 | "Exodus", 4 | "Leviticus", 5 | "Numbers", 6 | "Deuteronomy", 7 | "Joshua", 8 | "Judges", 9 | "Ruth", 10 | "1 Samuel", 11 | "2 Samuel", 12 | "1 Kings", 13 | "2 Kings", 14 | "1 Chronicles", 15 | "2 Chronicles", 16 | "Ezra", 17 | "Nehemiah", 18 | "Esther", 19 | "Job", 20 | "Psalm", 21 | "Proverbs", 22 | "Ecclesiastes", 23 | "Song of Solomon", 24 | "Isaiah", 25 | "Jeremiah", 26 | "Lamentations", 27 | "Ezekiel", 28 | "Daniel", 29 | "Hosea", 30 | "Joel", 31 | "Amos", 32 | "Obadiah", 33 | "Jonah", 34 | "Micah", 35 | "Nahum", 36 | "Habakkuk", 37 | "Zephaniah", 38 | "Haggai", 39 | "Zechariah", 40 | "Malachi", 41 | "Matthew", 42 | "Mark", 43 | "Luke", 44 | "John", 45 | "Acts", 46 | "Romans", 47 | "1 Corinthians", 48 | "2 Corinthians", 49 | "Galatians", 50 | "Ephesians", 51 | "Philippians", 52 | "Colossians", 53 | "1 Thessalonians", 54 | "2 Thessalonians", 55 | "1 Timothy", 56 | "2 Timothy", 57 | "Titus", 58 | "Philemon", 59 | "Hebrews", 60 | "James", 61 | "1 Peter", 62 | "2 Peter", 63 | "1 John", 64 | "2 John", 65 | "3 John", 66 | "Jude", 67 | "Revelation", 68 | ] 69 | 70 | BIBLE_CHOICES = [(book.lower(), book) for book in books_of_bible] 71 | -------------------------------------------------------------------------------- /apps/user/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.0 on 2022-04-15 22:03 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | initial = True 10 | 11 | dependencies = [ 12 | ("auth", "0012_alter_user_first_name_max_length"), 13 | ] 14 | 15 | operations = [ 16 | migrations.CreateModel( 17 | name="Profile", 18 | fields=[ 19 | ( 20 | "id", 21 | models.BigAutoField( 22 | auto_created=True, 23 | primary_key=True, 24 | serialize=False, 25 | verbose_name="ID", 26 | ), 27 | ), 28 | ("bio", models.TextField(blank=True, max_length=500)), 29 | ("role", models.CharField(blank=True, max_length=30)), 30 | ( 31 | "pic", 32 | models.ImageField( 33 | blank=True, default="media/avatar.png", upload_to="media/" 34 | ), 35 | ), 36 | ("social_link", models.URLField()), 37 | ( 38 | "user", 39 | models.OneToOneField( 40 | on_delete=django.db.models.deletion.CASCADE, to="auth.user" 41 | ), 42 | ), 43 | ], 44 | ), 45 | ] 46 | -------------------------------------------------------------------------------- /apps/user/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | from django.contrib.auth.models import User 3 | from .models import Profile 4 | 5 | 6 | class UserCreationForm(forms.ModelForm): 7 | password1 = forms.CharField(widget=forms.PasswordInput, label="Password") 8 | password2 = forms.CharField(widget=forms.PasswordInput, label="Repeat Password") 9 | 10 | class Meta: 11 | model = User 12 | fields = ["username", "first_name", "last_name", "email"] 13 | help_texts = {"username": None} 14 | 15 | def clean_first_name(self): 16 | first_name = self.cleaned_data["first_name"] 17 | if first_name.strip() == "": 18 | raise forms.ValidationError("First name is required") 19 | return first_name 20 | 21 | def clean_last_name(self): 22 | last_name = self.cleaned_data["last_name"] 23 | if last_name.strip() == "": 24 | raise forms.ValidationError("Last name is required") 25 | return last_name 26 | 27 | def clean_password2(self): 28 | password1 = self.cleaned_data.get("password1") 29 | password2 = self.cleaned_data.get("password2") 30 | if password1 != password2: 31 | raise forms.ValidationError("Passwords don't match") 32 | return password1 33 | 34 | 35 | class UserEditForm(forms.ModelForm): 36 | class Meta: 37 | model = User 38 | fields = ["username", "first_name", "last_name", "email"] 39 | help_texts = {"username": None} 40 | 41 | 42 | class ProfileForm(forms.ModelForm): 43 | class Meta: 44 | model = Profile 45 | fields = ["bio", "pic", "role", "social_link"] 46 | -------------------------------------------------------------------------------- /apps/search/templates/search/search_result.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% load static %} 3 | {% block title %}Search - {{query}}{% endblock title %} 4 | {% block style %}{% endblock style %} 5 | {% block current-page %}Search{% endblock current-page %} 6 | {% block content %} 7 |
8 |

You searched for this: {{ query }}

9 |
10 | {% if search_result %} 11 | {% with search_result|length as total_result %} 12 |

Found {{ total_result }} result{{ total_result|pluralize }}

13 |

Your search result:

14 | {% endwith %} 15 | 16 | {% for result in search_result %} 17 |
18 | pray 19 | 20 |
21 | 22 |

{{ result.title }}

23 |

{{ result.author }}

24 |
25 |
26 | {% endfor %} 27 | {% else %} 28 |

There is no result for this query.

29 | {% endif %} 30 |
31 | 32 |

Search again

33 |
34 | 35 |
36 |
37 | {% endblock %} -------------------------------------------------------------------------------- /apps/sermon/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.contrib.auth.models import User 3 | from django.urls import reverse 4 | from ckeditor.fields import RichTextField 5 | from taggit.managers import TaggableManager 6 | from datetime import date 7 | 8 | # Create your models here. 9 | class TodayManager(models.Manager): 10 | def get_queryset(self): 11 | return ( 12 | super(TodayManager, self).get_queryset().filter(date_created=date.today()) 13 | ) 14 | 15 | 16 | class Sermon(models.Model): 17 | """Class for Sermon DB Table""" 18 | 19 | title = models.CharField(max_length=100) 20 | author = models.ForeignKey(User, related_name="sermons", on_delete=models.CASCADE) 21 | image = models.ImageField(upload_to="media/") 22 | summary = models.CharField(max_length=150) 23 | content = RichTextField() 24 | datetime_created = models.DateTimeField(auto_now_add=True) 25 | date_created = models.DateField(auto_now_add=True) 26 | slug = models.SlugField(max_length=50) 27 | tags = TaggableManager() 28 | 29 | # managers 30 | objects = models.Manager() 31 | today = TodayManager() 32 | 33 | class Meta: 34 | ordering = ["-date_created"] 35 | unique_together = ["slug", "date_created"] 36 | 37 | def __str__(self) -> str: 38 | """Returns string representation for sermon object""" 39 | return self.title 40 | 41 | def get_absolute_url(self) -> str: 42 | """Returns canonical URL (sermon detail URL) for sermon object""" 43 | return reverse( 44 | "sermon:sermon_single", 45 | args=( 46 | self.date_created.year, 47 | self.date_created.month, 48 | self.date_created.day, 49 | self.slug, 50 | ), 51 | ) 52 | -------------------------------------------------------------------------------- /static/sermon/images/sermon_5.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /apps/user/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path, include 2 | from .views import profile, log_out, register, edit_profile 3 | from django.urls import reverse_lazy 4 | from django.contrib.auth.views import ( 5 | LoginView, 6 | PasswordResetView, 7 | PasswordChangeView, 8 | PasswordResetDoneView, 9 | PasswordResetConfirmView, 10 | PasswordChangeDoneView, 11 | PasswordResetCompleteView, 12 | ) 13 | 14 | app_name = "user" 15 | urlpatterns = [ 16 | path("logout/", log_out, name="log_out"), 17 | path("register/", register, name="register"), 18 | path("profile//", profile, name="profile"), 19 | path("profile/edit//", edit_profile, name="edit"), 20 | path("profile//", profile, name="profile"), 21 | path("login/", LoginView.as_view(), name="login"), 22 | path( 23 | "password_reset/", 24 | PasswordResetView.as_view(success_url=reverse_lazy("user:password_reset_done")), 25 | name="password_reset", 26 | ), 27 | path( 28 | "password_reset/done/", 29 | PasswordResetDoneView.as_view(), 30 | name="password_reset_done", 31 | ), 32 | path( 33 | "reset///", 34 | PasswordResetConfirmView.as_view( 35 | success_url=reverse_lazy("user:password_reset_conplete") 36 | ), 37 | name="password_reset_confirm", 38 | ), 39 | path( 40 | "reset/done", 41 | PasswordResetCompleteView.as_view(), 42 | name="password_reset_conplete", 43 | ), 44 | path( 45 | "password_change/", 46 | PasswordChangeView.as_view( 47 | success_url=reverse_lazy("user:password_change_done") 48 | ), 49 | name="password_change", 50 | ), 51 | path( 52 | "password_change/done", 53 | PasswordChangeDoneView.as_view(), 54 | name="password_change_done", 55 | ), 56 | ] 57 | -------------------------------------------------------------------------------- /apps/payment/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render, redirect 2 | from django.http import JsonResponse, HttpResponse 3 | from django.contrib.auth.decorators import login_required 4 | from django.conf import settings 5 | from django.urls import reverse 6 | from .forms import PaymentForm 7 | from .models import Donation 8 | import requests 9 | import datetime 10 | 11 | # Create your views here. 12 | transaction_url = "https://api.paystack.co/transaction/" 13 | 14 | 15 | @login_required(login_url="user:login") 16 | def transact(request): 17 | user = request.user 18 | if request.method == "POST": 19 | form = PaymentForm(request.POST) 20 | if form.is_valid(): 21 | instance = form.save(commit=False) 22 | instance.date = datetime.datetime.now() 23 | instance.save() 24 | print(instance.ref_id) 25 | return JsonResponse({"ref_id": instance.ref_id}) 26 | else: 27 | return redirect(reverse("payment:donate")) 28 | form = PaymentForm( 29 | initial={ 30 | "full_name": f"{user.first_name} {user.last_name}", 31 | "email_address": user.email, 32 | } 33 | ) 34 | context = {"form": form} 35 | return render(request, "payment/payment_form.html", context) 36 | 37 | 38 | def verfiy_payment(request, reference): 39 | headers = {"Authorization": f"Bearer {settings.PAYSTACK_SECRET}"} 40 | response = requests.get(f"{transaction_url}verify/{reference}", headers=headers) 41 | print(response.url) 42 | data = response.json() 43 | if data["data"]["status"] == "success": 44 | instance = Donation.objects.get(ref_id=reference) 45 | instance.paid = True 46 | instance.save() 47 | return JsonResponse(data, safe=False) 48 | 49 | 50 | def success(request): 51 | return render(request, "payment/success.html") 52 | 53 | 54 | def failure(request): 55 | return render(request, "payment/failure.html") 56 | -------------------------------------------------------------------------------- /static/sermon/images/sermon_3.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /apps/sermon/templates/sermon/sermon_pdf.html: -------------------------------------------------------------------------------- 1 | {% load static %} 2 | 3 | 4 |
5 |
6 | sermon image 7 |
8 | 9 | 10 |
11 | 12 |
13 |

{{ sermon.title }}

14 | 15 |
    16 |
  • 17 |
  • 18 |
  • 19 |
  • 20 |
  • 21 |
  • 22 |
23 | 24 |
25 |

Sermon by: {{ sermon.author.first_name }} {{ sermon.author.last_name }}

26 |

Tags: {{ sermon.tags.all|join:", " }}

27 |

Date: {{ sermon.date_created|date:"l j F, Y" }}

28 |
29 | 30 |
31 | 32 |
33 | {{ sermon.content|safe }} 34 |
35 | 36 |
37 |
38 | 39 | Author-Profile-Pic 40 | 41 |
42 |

{{ sermon.author.profile.bio }}

43 |

{{ sermon.author.profile.role }}

44 |
45 |
46 |
47 |
48 | 49 |
50 | 51 | -------------------------------------------------------------------------------- /apps/sermon/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.0 on 2022-04-15 22:03 2 | 3 | import ckeditor.fields 4 | from django.db import migrations, models 5 | import django.db.models.deletion 6 | import taggit.managers 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | initial = True 12 | 13 | dependencies = [ 14 | ("auth", "0012_alter_user_first_name_max_length"), 15 | ("taggit", "0004_alter_tag_id_alter_taggeditem_content_type_and_more"), 16 | ] 17 | 18 | operations = [ 19 | migrations.CreateModel( 20 | name="Sermon", 21 | fields=[ 22 | ( 23 | "id", 24 | models.BigAutoField( 25 | auto_created=True, 26 | primary_key=True, 27 | serialize=False, 28 | verbose_name="ID", 29 | ), 30 | ), 31 | ("title", models.CharField(max_length=100)), 32 | ("image", models.ImageField(upload_to="media/")), 33 | ("summary", models.CharField(max_length=150)), 34 | ("content", ckeditor.fields.RichTextField()), 35 | ("datetime_created", models.DateTimeField(auto_now_add=True)), 36 | ("date_created", models.DateField(auto_now_add=True)), 37 | ("slug", models.SlugField()), 38 | ( 39 | "author", 40 | models.ForeignKey( 41 | on_delete=django.db.models.deletion.CASCADE, 42 | related_name="sermons", 43 | to="auth.user", 44 | ), 45 | ), 46 | ( 47 | "tags", 48 | taggit.managers.TaggableManager( 49 | help_text="A comma-separated list of tags.", 50 | through="taggit.TaggedItem", 51 | to="taggit.Tag", 52 | verbose_name="Tags", 53 | ), 54 | ), 55 | ], 56 | options={ 57 | "ordering": ["-date_created"], 58 | "unique_together": {("slug", "date_created")}, 59 | }, 60 | ), 61 | ] 62 | -------------------------------------------------------------------------------- /static/payment/js/popup.js: -------------------------------------------------------------------------------- 1 | const paymentForm = document.getElementById('donate-btn'); 2 | const publicKey = JSON.parse(document.getElementById('public_key').textContent); 3 | const emailInput = document.getElementById("id_email_address"); 4 | const amountInput = document.getElementById("id_amount"); 5 | const fullNameInput = document.getElementById("id_full_name"); 6 | const csrfToken = document.querySelector("input[name='csrfmiddlewaretoken']"); 7 | 8 | 9 | paymentForm.addEventListener("click", payWithPaystack, false); 10 | function payWithPaystack(e) { 11 | e.preventDefault(); 12 | const data = { 13 | 'csrfmiddlewaretoken': csrfToken.value, 14 | 'full_name': fullNameInput.value, 15 | 'email_address': emailInput.value, 16 | 'amount': amountInput.value 17 | }; 18 | const saveFormUrl = window.location.href; 19 | let refId = saveFormToDB(data, saveFormUrl); 20 | let handler = PaystackPop.setup({ 21 | key: publicKey, // 22 | email: emailInput.value, 23 | amount: amountInput.value * 100, //* 100 so as to convert to naira 24 | ref: refId, // returns the ref_id in the db 25 | currency: 'NGN', 26 | onClose: function () { 27 | alert('Window closed.'); 28 | }, 29 | callback: function (response) { 30 | let reference = response.reference; 31 | // verify transaction 32 | $.ajax({ 33 | type: 'GET', 34 | url: `${window.location.href}verify/${reference}`, 35 | success: function (response) { 36 | console.log(response.data.status==="success"); 37 | if (response.data.status==="success"){ 38 | window.location = `${window.location.href}success/`; 39 | return; 40 | } 41 | window.location = `${window.location.href}failure/`; 42 | } 43 | }); 44 | } 45 | }); 46 | handler.openIframe(); 47 | } 48 | 49 | 50 | 51 | function saveFormToDB(data, url) { 52 | let refId = []; 53 | $.ajax({ 54 | type: 'POST', 55 | async: false, 56 | url: url, 57 | data: data, 58 | success: function (response) { 59 | console.log(response); 60 | refId.push(response.ref_id); 61 | 62 | } 63 | }); 64 | console.log(refId[0]); 65 | return refId[0]; 66 | } -------------------------------------------------------------------------------- /apps/blog/templates/blog/detail.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% load static %} 3 | {% block title %}{{ post.title }}{% endblock title %} 4 | {% block current-page %}Blog{% endblock current-page %} 5 | {% block content %} 6 |
7 |
8 |
9 | church light bulb 10 |
11 |

{{ post.date_created|date:"d" }}

12 |

{{ post.date_created|date:"N, Y" }}

13 |
14 |
15 | 16 |
17 |

{{ post.title }}

18 |

19 | By {{ post.author.first_name|title }} {{ post.author.last_name|title }} | In {{ post.category }} | {{ post.comment_set.count }} Comments 20 | {% if request.user == post.author %} 21 | |Edit 22 | | 23 | {% endif %} 24 |

25 |
26 | 27 |
28 | {{ post.body|safe }} 29 |
30 | 31 |
32 | {% for comment in comments %} 33 |
34 |
35 | 37 |

{{ comment.user.username|title }}
{{ comment.user.profile.bio }}

38 |

{{ comment.date_created }}

39 |
40 | {{ comment.comment_text }} 41 |
42 | {% empty %} 43 |

There is no comment yet.

44 | {% endfor %} 45 |
46 | 47 |
48 | {% csrf_token %} 49 | 50 | {{ form.comment_text }} 51 | 52 |
53 | 54 |
55 |
56 | {% endblock content %} -------------------------------------------------------------------------------- /static/sermon/images/sermon_2.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /apps/sermon/templates/sermon/detail.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% load static %} 3 | {% block style %} {% endblock style %} 4 | {% block title %}Sermon - {{ sermon.title }} {% endblock title %} 5 | 6 | {% block content %} 7 |
8 |
9 | sermon image 10 |
11 | 12 | 13 |
14 | 15 |
16 |

{{ sermon.title }}

17 | 18 | 28 | 29 |
30 |

Sermon by: {{ sermon.author.first_name }} {{ sermon.author.last_name }}

31 |

Tags: {{ sermon.tags.all|join:", " }}

32 |

Date: {{ sermon.date_created|date:"l j F, Y" }}

33 |

View: {{ total_views }} View{{ total_views|pluralize }}

34 |
35 | 36 |
37 | 38 |
39 | {{ sermon.content|safe }} 40 |
41 | 42 |
43 |
44 | 45 | Author-Profile-Pic 46 | 47 |
48 |

{{ sermon.author.profile.bio }}

49 |

{{ sermon.author.profile.role }}

50 |
51 |
52 |
53 |
54 | 55 |
56 | {% endblock content %} 57 | 58 | {% block js %} 59 | 60 | {% endblock js %} -------------------------------------------------------------------------------- /static/sermon/images/sermon_6.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /apps/user/views.py: -------------------------------------------------------------------------------- 1 | from django.core.exceptions import PermissionDenied 2 | from django.shortcuts import get_object_or_404, redirect, render, HttpResponse 3 | from django.contrib.auth import logout 4 | from .forms import UserCreationForm, ProfileForm, UserEditForm 5 | from django.contrib.auth.models import User 6 | from django.contrib import messages 7 | 8 | # Create your views here. 9 | def profile(request, user_id): 10 | user = get_object_or_404(User, id=user_id) 11 | profile = user.profile 12 | context = {"user": user, "profile": profile} 13 | return render(request, "user/profile.html", context) 14 | 15 | 16 | def edit_profile(request, user_id): 17 | user = get_object_or_404(User, id=user_id) 18 | profile = user.profile 19 | if request.user != user: 20 | raise PermissionDenied() 21 | if request.method != "POST": 22 | user_form = UserEditForm(instance=user) 23 | profile_form = ProfileForm(instance=profile) 24 | else: 25 | user_form = UserEditForm(request.POST, instance=user) 26 | profile_form = ProfileForm(request.POST, request.FILES, instance=profile) 27 | if user_form.is_valid() and profile_form.is_valid(): 28 | user_form.save() 29 | profile_form.save() 30 | messages.success(request, "Profile Update Successfully.") 31 | return redirect("user:profile", user_id=user.id) 32 | else: 33 | messages.error(request, "Error Updating your profile") 34 | context = {"user_form": user_form, "profile_form": profile_form} 35 | return render(request, "user/profile_edit.html", context) 36 | 37 | 38 | def register(request): 39 | if request.method != "POST": 40 | user_form = UserCreationForm() 41 | profile_form = ProfileForm() 42 | else: 43 | user_form = UserCreationForm(request.POST) 44 | profile_form = ProfileForm(request.POST, request.FILES) 45 | if user_form.is_valid() and profile_form.is_valid(): 46 | new_user = user_form.save(commit=False) 47 | new_profile = profile_form.save(commit=False) 48 | new_user.set_password(user_form.cleaned_data["password1"]) 49 | new_user.save() 50 | new_user.profile.bio = profile_form.cleaned_data["bio"] 51 | new_user.profile.pic = profile_form.cleaned_data["pic"] 52 | new_user.profile.social_link = profile_form.cleaned_data["social_link"] 53 | new_user.profile.role = profile_form.cleaned_data["role"] 54 | new_user.save() 55 | # new_profile.user = new_user 56 | # new_profile.save() 57 | messages.success(request, "Account created Successfully.") 58 | return redirect("user:login") 59 | else: 60 | messages.error(request, "Error creating your account") 61 | context = {"user_form": user_form, "profile_form": profile_form} 62 | return render(request, "user/register.html", context) 63 | 64 | 65 | def log_out(request): 66 | logout(request) 67 | messages.success(request, "Logout Successfully.") 68 | return redirect("blog:index") 69 | -------------------------------------------------------------------------------- /apps/sermon/views.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from django.conf import settings 4 | from django.db.models import Count 5 | from django.shortcuts import HttpResponse, get_object_or_404, render 6 | from django.template.loader import render_to_string 7 | 8 | from .models import Sermon 9 | 10 | # added to fix os.add_dll_directory(r"C:\Program Files\GTK3-Runtime Win64\bin") 11 | os.add_dll_directory(r"C:\Program Files\GTK3-Runtime Win64\bin") 12 | 13 | import json 14 | 15 | import redis 16 | import weasyprint 17 | 18 | # connect to redis 19 | r = redis.Redis( 20 | host=settings.REDIS_HOST, port=settings.REDIS_PORT, db=settings.REDIS_DB 21 | ) 22 | 23 | # Create your views here. 24 | def index(request): 25 | today_sermon = Sermon.today.first() 26 | if today_sermon: 27 | # increment today sermon views by 1 28 | total_views = r.get(f"sermon:{today_sermon.id}:views") 29 | if total_views is None: 30 | total_views = "0" 31 | total_views = json.loads(total_views) 32 | today_sermon_tags_ids = today_sermon.tags.values_list("id", flat=True) 33 | # gets similar sermon based on the number of tags in common 34 | similar_sermons = Sermon.objects.filter(tags__in=today_sermon_tags_ids).exclude( 35 | id=today_sermon.id 36 | ) 37 | similar_sermons = similar_sermons.annotate(same_tags=Count("tags")).order_by( 38 | "-same_tags", "-datetime_created" 39 | )[:2] 40 | # gets popular sermon based on the number of views 41 | sermon_ranking = r.zrange("sermon_ranking", 0, -1, desc=True)[:5] 42 | sermon_ranking_ids = [int(id) for id in sermon_ranking] 43 | popular_sermons = list(Sermon.objects.filter(id__in=sermon_ranking_ids)) 44 | # sort the popular_sermons objects to be in sync with the sermon_ranking_ids 45 | popular_sermons.sort(key=lambda sermon: sermon_ranking_ids.index(sermon.id)) 46 | context = { 47 | "today_sermon": today_sermon, 48 | "similar_sermons": similar_sermons, 49 | "total_views": total_views, 50 | "popular_sermons": popular_sermons, 51 | } 52 | return render(request, "sermon/index.html", context) 53 | else: 54 | return HttpResponse("No sermon has been added today! Thank You.") 55 | 56 | 57 | def detail(request, year, month, day, slug): 58 | sermon = get_object_or_404( 59 | Sermon, 60 | date_created__year=year, 61 | date_created__month=month, 62 | date_created__day=day, 63 | slug=slug, 64 | ) 65 | # increment today sermon views by 1 66 | total_views = r.incr(f"sermon:{sermon.id}:views") 67 | # increment sermon ranking by 1 68 | r.zincrby("sermon_ranking", 1, sermon.id) 69 | context = {"sermon": sermon, "total_views": total_views} 70 | return render(request, "sermon/detail.html", context) 71 | 72 | 73 | def sermon_pdf(request, id): 74 | # generate the pdf of each sermon 75 | sermon = get_object_or_404(Sermon, id=id) 76 | html = render_to_string("sermon/sermon_pdf.html", {"sermon": sermon}) 77 | response = HttpResponse(content_type="application/pdf") 78 | response["Content-Disposition"] = f"filename=sermon_{sermon.slug}.pdf" 79 | weasyprint.HTML(string=html, base_url=request.build_absolute_uri()).write_pdf( 80 | response, 81 | stylesheets=[ 82 | weasyprint.CSS(settings.STATIC_ROOT + "sermon/css/sermon.css"), 83 | weasyprint.CSS(settings.STATIC_ROOT + "blog/css/base.css"), 84 | ], 85 | ) 86 | return response 87 | -------------------------------------------------------------------------------- /static/sermon/images/sermon_1.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /apps/blog/templates/blog/index.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% load sermon_tags %} 3 | {% load static %} 4 | {% block title %}Believe - Home {% endblock title %} 5 | 6 | {% block content %} 7 |
8 |
9 |
10 | {% for post in page_obj %} 11 |
12 |
13 | church light bulb 14 |
15 |

{{ post.date_created|date:"d" }}

16 |

{{ post.date_created|date:"N, Y" }}

17 |
18 |
19 | 20 |
21 |

{{ post.title }}

22 |

23 | By {{ post.author.first_name|title }} {{ post.author.last_name|title }}| In {{ post.category }} | {{ post.comment_set.count }} 24 | Comments 25 |

26 |
27 | 28 |
29 | {{ post.description|safe }} 30 |
31 | 32 | Read More 33 |
34 | 35 | {% endfor %} 36 | 37 | 54 | 55 |
56 | 57 | 96 |
97 |
98 | {% endblock content %} -------------------------------------------------------------------------------- /apps/blog/models.py: -------------------------------------------------------------------------------- 1 | from datetime import date 2 | 3 | import requests 4 | from ckeditor.fields import RichTextField 5 | from django.contrib import messages 6 | from django.contrib.auth import get_user_model 7 | from django.db import models 8 | from django.urls import reverse 9 | from django.utils.text import slugify 10 | from apps.utils.bible_books import BIBLE_CHOICES 11 | 12 | User = get_user_model() 13 | 14 | 15 | class Category(models.Model): 16 | """class for Category DB Table""" 17 | 18 | category_name = models.CharField(max_length=20, unique=True) 19 | 20 | class Meta: 21 | verbose_name_plural = "categories" 22 | 23 | def __str__(self) -> str: 24 | """Returns string representation of category object""" 25 | return self.category_name 26 | 27 | 28 | class Post(models.Model): 29 | """Class for Post DB Table""" 30 | 31 | title = models.CharField(max_length=100, unique=True) 32 | slug = models.SlugField(max_length=50, blank=True) 33 | author = models.ForeignKey(User, on_delete=models.CASCADE) 34 | description = models.CharField(max_length=1000) 35 | date_created = models.DateTimeField(auto_now_add=True) 36 | last_modified = models.DateTimeField(auto_now=True) 37 | post_image = models.ImageField(upload_to="media/") 38 | body = RichTextField() 39 | category = models.ForeignKey(Category, on_delete=models.CASCADE) 40 | 41 | class Meta: 42 | ordering = ["-last_modified"] 43 | 44 | def __str__(self) -> str: 45 | """Returns string representation of post object""" 46 | return self.title 47 | 48 | def get_absolute_url(self) -> str: 49 | """Returns the canonical URL (post detail URL) for post object""" 50 | return reverse( 51 | "blog:detail", 52 | args=( 53 | self.id, 54 | self.slug, 55 | ), 56 | ) 57 | 58 | def save(self, *args, **kwargs): 59 | """Save method includes slugify""" 60 | if not self.slug: 61 | self.slug = slugify(self.title) 62 | super().save(*args, **kwargs) 63 | 64 | 65 | class Comment(models.Model): 66 | """Class for Comment DB Table""" 67 | 68 | user = models.ForeignKey(User, on_delete=models.CASCADE) 69 | comment_text = models.TextField() 70 | post = models.ForeignKey(Post, on_delete=models.CASCADE) 71 | date_created = models.DateTimeField(auto_now_add=True) 72 | last_modified = models.DateTimeField(auto_now=True) 73 | 74 | def __str__(self) -> str: 75 | """Returns string representation of Comment object""" 76 | return self.comment_text 77 | 78 | 79 | class Announcement(models.Model): 80 | """Class for Announcement DB Table""" 81 | 82 | event_name = models.CharField(max_length=50) 83 | featured = models.BooleanField() # only set one announcement to be True 84 | event_date = models.DateTimeField(blank=True) 85 | 86 | # order the announcement of the events in order of the closet event-date 87 | # the closet event date comes first 88 | class Meta: 89 | ordering = ["event_date"] 90 | 91 | def __str__(self) -> str: 92 | """Returns string representation of Announcement object""" 93 | return self.event_name 94 | 95 | 96 | class TodayBibleVerse(models.Manager): 97 | def get_queryset(self): 98 | return super(TodayBibleVerse, self).get_queryset().filter(date_for=date.today()) 99 | 100 | 101 | class BibleVerse(models.Model): 102 | """Class for Bible Verse DB Table""" 103 | 104 | bible_verse = models.CharField(max_length=20, choices=BIBLE_CHOICES) 105 | ref = models.CharField( 106 | max_length=10, 107 | help_text="Write in this format: \ 108 | chapter:verse and chapter:start-end in case of range", 109 | ) 110 | date_for = models.DateField() 111 | 112 | objects = models.Manager() 113 | today = TodayBibleVerse() 114 | 115 | def __str__(self) -> str: 116 | """Returns string representation of BibleVerse object""" 117 | return f"{self.bible_verse} {self.ref}" 118 | 119 | def get_bible_verse(self) -> str: 120 | """Returns the content for the bible_verse reference""" 121 | verse_ref = str(self) 122 | url = "https://bible-api.com/" + verse_ref 123 | response = requests.get(url).json() 124 | text = response["text"] 125 | return text 126 | -------------------------------------------------------------------------------- /apps/blog/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.0 on 2022-04-15 22:03 2 | 3 | import ckeditor.fields 4 | from django.db import migrations, models 5 | import django.db.models.deletion 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | initial = True 11 | 12 | dependencies = [ 13 | ("auth", "0012_alter_user_first_name_max_length"), 14 | ] 15 | 16 | operations = [ 17 | migrations.CreateModel( 18 | name="Announcement", 19 | fields=[ 20 | ( 21 | "id", 22 | models.BigAutoField( 23 | auto_created=True, 24 | primary_key=True, 25 | serialize=False, 26 | verbose_name="ID", 27 | ), 28 | ), 29 | ("event_name", models.CharField(max_length=50)), 30 | ("featured", models.BooleanField()), 31 | ("event_date", models.DateTimeField(blank=True)), 32 | ], 33 | options={ 34 | "ordering": ["event_date"], 35 | }, 36 | ), 37 | migrations.CreateModel( 38 | name="Category", 39 | fields=[ 40 | ( 41 | "id", 42 | models.BigAutoField( 43 | auto_created=True, 44 | primary_key=True, 45 | serialize=False, 46 | verbose_name="ID", 47 | ), 48 | ), 49 | ("category_name", models.CharField(max_length=20, unique=True)), 50 | ], 51 | options={ 52 | "verbose_name_plural": "categories", 53 | }, 54 | ), 55 | migrations.CreateModel( 56 | name="Post", 57 | fields=[ 58 | ( 59 | "id", 60 | models.BigAutoField( 61 | auto_created=True, 62 | primary_key=True, 63 | serialize=False, 64 | verbose_name="ID", 65 | ), 66 | ), 67 | ("title", models.CharField(max_length=100, unique=True)), 68 | ("slug", models.SlugField()), 69 | ("description", models.CharField(max_length=1000)), 70 | ("date_created", models.DateTimeField(auto_now_add=True)), 71 | ("last_modified", models.DateTimeField(auto_now=True)), 72 | ("post_image", models.ImageField(upload_to="media/")), 73 | ("body", ckeditor.fields.RichTextField()), 74 | ( 75 | "author", 76 | models.ForeignKey( 77 | on_delete=django.db.models.deletion.CASCADE, to="auth.user" 78 | ), 79 | ), 80 | ( 81 | "category", 82 | models.ForeignKey( 83 | on_delete=django.db.models.deletion.CASCADE, to="blog.category" 84 | ), 85 | ), 86 | ], 87 | options={ 88 | "ordering": ["-last_modified"], 89 | }, 90 | ), 91 | migrations.CreateModel( 92 | name="Comment", 93 | fields=[ 94 | ( 95 | "id", 96 | models.BigAutoField( 97 | auto_created=True, 98 | primary_key=True, 99 | serialize=False, 100 | verbose_name="ID", 101 | ), 102 | ), 103 | ("comment_text", models.TextField()), 104 | ("date_created", models.DateTimeField(auto_now_add=True)), 105 | ("last_modified", models.DateTimeField(auto_now=True)), 106 | ( 107 | "post", 108 | models.ForeignKey( 109 | on_delete=django.db.models.deletion.CASCADE, to="blog.post" 110 | ), 111 | ), 112 | ( 113 | "user", 114 | models.ForeignKey( 115 | on_delete=django.db.models.deletion.CASCADE, to="auth.user" 116 | ), 117 | ), 118 | ], 119 | ), 120 | ] 121 | -------------------------------------------------------------------------------- /apps/blog/newsletter.py: -------------------------------------------------------------------------------- 1 | import hashlib 2 | import json 3 | 4 | import mailchimp_marketing as MailchimpMarketing 5 | from decouple import config 6 | from mailchimp_marketing.api_client import ApiClientError 7 | 8 | 9 | class Newsletter: 10 | """ 11 | newsletter class using mailchimp. 12 | 13 | Read mailchimp docs to know the required 14 | fields in member_info so as to know the 15 | keyword arguments to use. 16 | """ 17 | 18 | @staticmethod 19 | def get_email_hash(email): 20 | """returns MD5 hash of the lowercase version of the email address.""" 21 | email_hash = hashlib.md5(email.encode("utf-8").lower()).hexdigest() 22 | return email_hash 23 | 24 | @staticmethod 25 | def serialize_error(error: ApiClientError) -> dict: 26 | """ 27 | convert apiclient error to dict 28 | type(error.text) -> str so it has to be converted to dict 29 | to access the key and values 30 | """ 31 | return json.loads(error.text) 32 | 33 | def __init__(self, list_id: str) -> None: 34 | """initialize connection to mailchimp""" 35 | self.mailchimp = MailchimpMarketing.Client() 36 | self.mailchimp.set_config( 37 | {"api_key": config("MAILCHIMP_KEY"), "server": config("MAILCHIMP_SERVER")} 38 | ) 39 | self.list_id = list_id 40 | 41 | def __str__(self) -> str: 42 | """string representation of the newsletter object""" 43 | return f"Newsletter object for {self.list_id}" 44 | 45 | def __eq__(self, other_object) -> bool: 46 | """compares two newsletter objects""" 47 | if isinstance(other_object, Newsletter): 48 | return self.list_id == other_object.list_id 49 | return False 50 | 51 | def add_member(self, is_exist_subscribe: bool = True, **member_info): 52 | """ 53 | adds member to the audience list with the member_info. 54 | 55 | is_exist_subscribe determines whether user whose email_address 56 | already exist in the list should be updated to change their status to 57 | subscribed. This is useful as users who have unsubscribed might want 58 | to subscribe back. Moreso, when a user unsubsribed their email_address 59 | isn't removed from the list only their status is changed. 60 | 61 | More info on mailchimp docs as regards status and removing members. 62 | """ 63 | try: 64 | response = self.mailchimp.lists.add_list_member(self.list_id, member_info) 65 | return "added" 66 | except ApiClientError as error: 67 | response = self.get_member_info( 68 | self.get_email_hash(member_info["email_address"]) 69 | ) 70 | error = Newsletter.serialize_error(error) 71 | if ( 72 | error["title"] == "Member Exists" 73 | and response["status"] == "unsubscribed" 74 | ): 75 | if is_exist_subscribe: 76 | return self.subscribe(response["contact_id"]) 77 | return error["title"] 78 | 79 | def get_all_members(self): 80 | """gets all the members in a list""" 81 | try: 82 | response = self.mailchimp.lists.get_list_members_info(self.list_id) 83 | return response 84 | except ApiClientError as error: 85 | error = Newsletter.serialize_error(error) 86 | return error 87 | 88 | def get_member_info(self, member_email_hash): 89 | """gets info of a specific member""" 90 | try: 91 | response = self.mailchimp.lists.get_list_member( 92 | self.list_id, member_email_hash 93 | ) 94 | return response 95 | except ApiClientError as error: 96 | error = Newsletter.serialize_error(error) 97 | return error 98 | 99 | def update_contact(self, member_email_hash, **member_info): 100 | """update the member info of the member that owns the email_hash""" 101 | try: 102 | response = self.mailchimp.lists.update_list_member( 103 | self.list_id, member_email_hash, member_info 104 | ) 105 | return "updated" 106 | except ApiClientError as error: 107 | error = Newsletter.serialize_error(error) 108 | return error 109 | 110 | def subscribe(self, member_email_hash): 111 | """change a specific member status to subscribed""" 112 | return self.update_contact(member_email_hash, status="subscribed") 113 | 114 | def unsubscribe(self, member_email_hash): 115 | """change a specific member status to unsubscribed""" 116 | return self.update_contact(member_email_hash, status="unsubscribed") 117 | 118 | 119 | list_id = "a8eb3204d1" 120 | newsletter = Newsletter(list_id) 121 | -------------------------------------------------------------------------------- /apps/blog/migrations/0002_bibleverse.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.0 on 2022-04-21 11:44 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("blog", "0001_initial"), 10 | ] 11 | 12 | operations = [ 13 | migrations.CreateModel( 14 | name="BibleVerse", 15 | fields=[ 16 | ( 17 | "id", 18 | models.BigAutoField( 19 | auto_created=True, 20 | primary_key=True, 21 | serialize=False, 22 | verbose_name="ID", 23 | ), 24 | ), 25 | ( 26 | "bible_verse", 27 | models.CharField( 28 | choices=[ 29 | ("genesis", "Genesis"), 30 | ("exodus", "Exodus"), 31 | ("leviticus", "Leviticus"), 32 | ("numbers", "Numbers"), 33 | ("deuteronomy", "Deuteronomy"), 34 | ("joshua", "Joshua"), 35 | ("judges", "Judges"), 36 | ("ruth", "Ruth"), 37 | ("1 samuel", "1 Samuel"), 38 | ("2 samuel", "2 Samuel"), 39 | ("1 kings", "1 Kings"), 40 | ("2 kings", "2 Kings"), 41 | ("1 chronicles", "1 Chronicles"), 42 | ("2 chronicles", "2 Chronicles"), 43 | ("ezra", "Ezra"), 44 | ("nehemiah", "Nehemiah"), 45 | ("esther", "Esther"), 46 | ("job", "Job"), 47 | ("psalm", "Psalm"), 48 | ("proverbs", "Proverbs"), 49 | ("ecclesiastes", "Ecclesiastes"), 50 | ("song of solomon", "Song of Solomon"), 51 | ("isaiah", "Isaiah"), 52 | ("jeremiah", "Jeremiah"), 53 | ("lamentations", "Lamentations"), 54 | ("ezekiel", "Ezekiel"), 55 | ("daniel", "Daniel"), 56 | ("hosea", "Hosea"), 57 | ("joel", "Joel"), 58 | ("amos", "Amos"), 59 | ("obadiah", "Obadiah"), 60 | ("jonah", "Jonah"), 61 | ("micah", "Micah"), 62 | ("nahum", "Nahum"), 63 | ("habakkuk", "Habakkuk"), 64 | ("zephaniah", "Zephaniah"), 65 | ("haggai", "Haggai"), 66 | ("zechariah", "Zechariah"), 67 | ("malachi", "Malachi"), 68 | ("matthew", "Matthew"), 69 | ("mark", "Mark"), 70 | ("luke", "Luke"), 71 | ("john", "John"), 72 | ("acts", "Acts"), 73 | ("romans", "Romans"), 74 | ("1 corinthians", "1 Corinthians"), 75 | ("2 corinthians", "2 Corinthians"), 76 | ("galatians", "Galatians"), 77 | ("ephesians", "Ephesians"), 78 | ("philippians", "Philippians"), 79 | ("colossians", "Colossians"), 80 | ("1 thessalonians", "1 Thessalonians"), 81 | ("2 thessalonians", "2 Thessalonians"), 82 | ("1 timothy", "1 Timothy"), 83 | ("2 timothy", "2 Timothy"), 84 | ("titus", "Titus"), 85 | ("philemon", "Philemon"), 86 | ("hebrews", "Hebrews"), 87 | ("james", "James"), 88 | ("1 peter", "1 Peter"), 89 | ("2 peter", "2 Peter"), 90 | ("1 john", "1 John"), 91 | ("2 john", "2 John"), 92 | ("3 john", "3 John"), 93 | ("jude", "Jude"), 94 | ("revelation", "Revelation"), 95 | ], 96 | max_length=20, 97 | ), 98 | ), 99 | ( 100 | "ref", 101 | models.CharField( 102 | help_text="Write in this format: chapter:verse and chapter:start-end in case of range", 103 | max_length=10, 104 | ), 105 | ), 106 | ("date_for", models.DateField()), 107 | ], 108 | ), 109 | ] 110 | -------------------------------------------------------------------------------- /config/settings/base.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | import os 3 | from decouple import config 4 | from django.contrib.messages import constants as messages 5 | 6 | # django 4 does not support force_text 7 | # here is a hack to fix it 8 | import django 9 | from django.utils.encoding import force_str 10 | 11 | django.utils.encoding.force_text = force_str 12 | 13 | # allows to use bootstrap class as the 14 | # tags for their respective messages 15 | MESSAGE_TAGS = { 16 | messages.DEBUG: "alert-secondary", 17 | messages.INFO: "alert-info", 18 | messages.SUCCESS: "alert-success", 19 | messages.WARNING: "alert-warning", 20 | messages.ERROR: "alert-danger", 21 | } 22 | 23 | 24 | # Build paths inside the project like this: BASE_DIR / 'subdir'. 25 | BASE_DIR = Path(__file__).resolve().parent.parent.parent 26 | 27 | 28 | # Quick-start development settings - unsuitable for production 29 | # See https://docs.djangoproject.com/en/3.2/howto/deployment/checklist/ 30 | 31 | # SECURITY WARNING: keep the secret key used in production secret! 32 | SECRET_KEY = config("SECRET_KEY") 33 | 34 | # SECURITY WARNING: don't run with debug turned on in production! 35 | DEBUG = config("DEBUG", cast=bool) 36 | 37 | ALLOWED_HOSTS = [] 38 | 39 | 40 | # Application definition 41 | 42 | LOCAL_APPS = [ 43 | # custom apps 44 | "apps.user", 45 | "apps.blog", 46 | "apps.sermon", 47 | "apps.search", 48 | "apps.payment", 49 | "apps.utils", 50 | ] 51 | 52 | DJANGO_APPS = [ 53 | # default django-apps 54 | "django.contrib.admin", 55 | "django.contrib.auth", 56 | "django.contrib.contenttypes", 57 | "django.contrib.sessions", 58 | "django.contrib.messages", 59 | "django.contrib.staticfiles", 60 | "django.contrib.sites", 61 | "django.contrib.sitemaps", 62 | "django.contrib.postgres", 63 | ] 64 | 65 | THIRD_PARTY_APPS = [ 66 | "ckeditor", 67 | "taggit", 68 | "django_celery_beat", 69 | ] 70 | 71 | # See: https://docs.djangoproject.com/en/dev/ref/settings/#installed-apps 72 | # local apps shall override djangos default, so order is important 73 | INSTALLED_APPS = LOCAL_APPS + DJANGO_APPS + THIRD_PARTY_APPS 74 | 75 | MIDDLEWARE = [ 76 | "django.middleware.security.SecurityMiddleware", 77 | "django.contrib.sessions.middleware.SessionMiddleware", 78 | "django.middleware.common.CommonMiddleware", 79 | "django.middleware.csrf.CsrfViewMiddleware", 80 | "django.contrib.auth.middleware.AuthenticationMiddleware", 81 | "django.contrib.messages.middleware.MessageMiddleware", 82 | "django.middleware.clickjacking.XFrameOptionsMiddleware", 83 | ] 84 | 85 | ROOT_URLCONF = "church.urls" 86 | 87 | TEMPLATES = [ 88 | { 89 | "BACKEND": "django.template.backends.django.DjangoTemplates", 90 | "DIRS": [BASE_DIR / "templates"], 91 | "APP_DIRS": True, 92 | "OPTIONS": { 93 | "context_processors": [ 94 | "apps.blog.context_processors.announcement", 95 | "django.template.context_processors.debug", 96 | "django.template.context_processors.request", 97 | "django.contrib.auth.context_processors.auth", 98 | "django.contrib.messages.context_processors.messages", 99 | ], 100 | }, 101 | }, 102 | ] 103 | 104 | WSGI_APPLICATION = "church.wsgi.application" 105 | 106 | 107 | # Database 108 | # https://docs.djangoproject.com/en/3.2/ref/settings/#databases 109 | DATABASES = { 110 | "default": { 111 | "ENGINE": "django.db.backends.postgresql", 112 | "NAME": "church", 113 | "USER": "postgres", 114 | "PASSWORD": config("DB_PASSWORD"), 115 | "HOST": "localhost", 116 | "PORT": 5432, 117 | } 118 | } 119 | 120 | # Password validation 121 | # https://docs.djangoproject.com/en/3.2/ref/settings/#auth-password-validators 122 | 123 | AUTH_PASSWORD_VALIDATORS = [ 124 | { 125 | "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", 126 | }, 127 | { 128 | "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", 129 | }, 130 | { 131 | "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", 132 | }, 133 | { 134 | "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", 135 | }, 136 | ] 137 | 138 | 139 | # Internationalization 140 | # https://docs.djangoproject.com/en/3.2/topics/i18n/ 141 | 142 | LANGUAGE_CODE = "en-us" 143 | 144 | TIME_ZONE = "Africa/Lagos" 145 | 146 | USE_I18N = True 147 | 148 | USE_L10N = True 149 | 150 | USE_TZ = True 151 | 152 | 153 | # Static files (CSS, JavaScript, Images) 154 | # https://docs.djangoproject.com/en/3.2/howto/static-files/ 155 | 156 | STATIC_URL = "/static/" 157 | STATICFILES_DIRS = [ 158 | os.path.join(BASE_DIR, "static"), 159 | ] 160 | STATIC_ROOT = os.path.join(BASE_DIR, "staticfiles/") 161 | 162 | 163 | MEDIA_URL = "/media/" 164 | # MEDIA_ROOT = os.path.join(BASE_DIR, 'media') 165 | FILE_UPLOAD_PERMISSIONS = 0o640 166 | 167 | # Default primary key field type 168 | # https://docs.djangoproject.com/en/3.2/ref/settings/#default-auto-field 169 | 170 | DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" 171 | LOGIN_REDIRECT_URL = "blog:index" 172 | LOGIN_URL = "user:login" 173 | 174 | # Sitemap settings 175 | 176 | SITE_ID = 1 177 | -------------------------------------------------------------------------------- /config/settings/production.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | import os 3 | from decouple import config 4 | from django.contrib.messages import constants as messages 5 | 6 | # allows to use bootstrap class as the 7 | # tags for their respective messages 8 | MESSAGE_TAGS = { 9 | messages.DEBUG: "alert-secondary", 10 | messages.INFO: "alert-info", 11 | messages.SUCCESS: "alert-success", 12 | messages.WARNING: "alert-warning", 13 | messages.ERROR: "alert-danger", 14 | } 15 | 16 | 17 | # Build paths inside the project like this: BASE_DIR / 'subdir'. 18 | BASE_DIR = Path(__file__).resolve().parent.parent.parent 19 | 20 | 21 | # Quick-start development settings - unsuitable for production 22 | # See https://docs.djangoproject.com/en/3.2/howto/deployment/checklist/ 23 | 24 | # SECURITY WARNING: keep the secret key used in production secret! 25 | SECRET_KEY = config("SECRET_KEY") 26 | 27 | # SECURITY WARNING: don't run with debug turned on in production! 28 | DEBUG = config("DEBUG", cast=bool) 29 | 30 | ALLOWED_HOSTS = ["cbelieve.herokuapp.com"] 31 | 32 | 33 | # Application definition 34 | 35 | LOCAL_APPS = [ 36 | # custom apps 37 | "apps.user", 38 | "apps.blog", 39 | "apps.sermon", 40 | "apps.search", 41 | "apps.payment", 42 | "apps.utils", 43 | ] 44 | 45 | DJANGO_APPS = [ 46 | # default django-apps 47 | "django.contrib.admin", 48 | "django.contrib.auth", 49 | "django.contrib.contenttypes", 50 | "django.contrib.sessions", 51 | "django.contrib.messages", 52 | "django.contrib.staticfiles", 53 | "django.contrib.sites", 54 | "django.contrib.sitemaps", 55 | "django.contrib.postgres", 56 | ] 57 | 58 | THIRD_PARTY_APPS = [ 59 | "ckeditor", 60 | "storages", 61 | "taggit", 62 | "django_celery_beat", 63 | ] 64 | 65 | # See: https://docs.djangoproject.com/en/dev/ref/settings/#installed-apps 66 | # local apps shall override djangos default, so order is important 67 | INSTALLED_APPS = LOCAL_APPS + DJANGO_APPS + THIRD_PARTY_APPS 68 | 69 | MIDDLEWARE = [ 70 | "django.middleware.security.SecurityMiddleware", 71 | "django.contrib.sessions.middleware.SessionMiddleware", 72 | "django.middleware.common.CommonMiddleware", 73 | "django.middleware.csrf.CsrfViewMiddleware", 74 | "django.contrib.auth.middleware.AuthenticationMiddleware", 75 | "django.contrib.messages.middleware.MessageMiddleware", 76 | "django.middleware.clickjacking.XFrameOptionsMiddleware", 77 | ] 78 | 79 | ROOT_URLCONF = "church.urls" 80 | 81 | TEMPLATES = [ 82 | { 83 | "BACKEND": "django.template.backends.django.DjangoTemplates", 84 | "DIRS": [BASE_DIR / "templates"], 85 | "APP_DIRS": True, 86 | "OPTIONS": { 87 | "context_processors": [ 88 | "blog.context_processors.announcement", 89 | "django.template.context_processors.debug", 90 | "django.template.context_processors.request", 91 | "django.contrib.auth.context_processors.auth", 92 | "django.contrib.messages.context_processors.messages", 93 | ], 94 | }, 95 | }, 96 | ] 97 | 98 | WSGI_APPLICATION = "church.wsgi.application" 99 | 100 | 101 | # Database 102 | # https://docs.djangoproject.com/en/3.2/ref/settings/#databases 103 | 104 | DATABASES = { 105 | "default": { 106 | "ENGINE": "django.db.backends.postgresql", 107 | } 108 | } 109 | 110 | import dj_database_url 111 | 112 | db_from_env = dj_database_url.config(conn_max_age=600) 113 | DATABASES["default"].update(db_from_env) 114 | 115 | # Password validation 116 | # https://docs.djangoproject.com/en/3.2/ref/settings/#auth-password-validators 117 | 118 | AUTH_PASSWORD_VALIDATORS = [ 119 | { 120 | "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", 121 | }, 122 | { 123 | "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", 124 | }, 125 | { 126 | "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", 127 | }, 128 | { 129 | "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", 130 | }, 131 | ] 132 | 133 | 134 | # Internationalization 135 | # https://docs.djangoproject.com/en/3.2/topics/i18n/ 136 | 137 | LANGUAGE_CODE = "en-us" 138 | 139 | TIME_ZONE = "Africa/Lagos" 140 | 141 | USE_I18N = True 142 | 143 | USE_L10N = True 144 | 145 | USE_TZ = True 146 | 147 | 148 | # Static files (CSS, JavaScript, Images) 149 | # https://docs.djangoproject.com/en/3.2/howto/static-files/ 150 | 151 | STATIC_URL = "/static/" 152 | STATICFILES_DIRS = [ 153 | os.path.join(BASE_DIR, "static"), 154 | ] 155 | STATIC_ROOT = os.path.join(BASE_DIR, "staticfiles/") 156 | 157 | MEDIA_URL = "/media/" 158 | MEDIA_ROOT = os.path.join(BASE_DIR, "media") 159 | FILE_UPLOAD_PERMISSIONS = 0o640 160 | 161 | # Default primary key field type 162 | # https://docs.djangoproject.com/en/3.2/ref/settings/#default-auto-field 163 | 164 | DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" 165 | LOGIN_REDIRECT_URL = "blog:index" 166 | LOGIN_URL = "user:login" 167 | 168 | # aws s3 bucket config 169 | AWS_ACCESS_KEY_ID = config("AWS_ACCESS_KEY_ID") 170 | AWS_SECRET_ACCESS_KEY = config("AWS_SECRET_ACCESS_KEY") 171 | AWS_STORAGE_BUCKET_NAME = "churchbelieve" 172 | AWS_QUERYSTRING_AUTH = False 173 | DEFAULT_FILE_STORAGE = "storages.backends.s3boto3.S3Boto3Storage" 174 | STATICFILES_STORAGE = "storages.backends.s3boto3.S3Boto3Storage" 175 | -------------------------------------------------------------------------------- /config/settings/local.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | import os 3 | from decouple import config 4 | from django.contrib.messages import constants as messages 5 | 6 | # allows to use bootstrap class as the 7 | # tags for their respective messages 8 | MESSAGE_TAGS = { 9 | messages.DEBUG: "alert-secondary", 10 | messages.INFO: "alert-info", 11 | messages.SUCCESS: "alert-success", 12 | messages.WARNING: "alert-warning", 13 | messages.ERROR: "alert-danger", 14 | } 15 | 16 | 17 | # Build paths inside the project like this: BASE_DIR / 'subdir'. 18 | BASE_DIR = Path(__file__).resolve().parent.parent.parent 19 | 20 | 21 | # Quick-start development settings - unsuitable for production 22 | # See https://docs.djangoproject.com/en/3.2/howto/deployment/checklist/ 23 | 24 | # SECURITY WARNING: keep the secret key used in production secret! 25 | SECRET_KEY = config("SECRET_KEY") 26 | 27 | # SECURITY WARNING: don't run with debug turned on in production! 28 | DEBUG = config("DEBUG", cast=bool) 29 | 30 | ALLOWED_HOSTS = [] 31 | 32 | 33 | # Application definition 34 | 35 | LOCAL_APPS = [ 36 | # custom apps 37 | "apps.user", 38 | "apps.blog", 39 | "apps.sermon", 40 | "apps.search", 41 | "apps.payment", 42 | "apps.utils", 43 | ] 44 | 45 | DJANGO_APPS = [ 46 | # default django-apps 47 | "django.contrib.admin", 48 | "django.contrib.auth", 49 | "django.contrib.contenttypes", 50 | "django.contrib.sessions", 51 | "django.contrib.messages", 52 | "django.contrib.staticfiles", 53 | "django.contrib.sites", 54 | "django.contrib.sitemaps", 55 | "django.contrib.postgres", 56 | ] 57 | 58 | THIRD_PARTY_APPS = [ 59 | "ckeditor", 60 | "taggit", 61 | "django_celery_beat", 62 | ] 63 | 64 | # See: https://docs.djangoproject.com/en/dev/ref/settings/#installed-apps 65 | # local apps shall override djangos default, so order is important 66 | INSTALLED_APPS = LOCAL_APPS + DJANGO_APPS + THIRD_PARTY_APPS 67 | 68 | MIDDLEWARE = [ 69 | "django.middleware.security.SecurityMiddleware", 70 | "django.contrib.sessions.middleware.SessionMiddleware", 71 | "django.middleware.common.CommonMiddleware", 72 | "django.middleware.csrf.CsrfViewMiddleware", 73 | "django.contrib.auth.middleware.AuthenticationMiddleware", 74 | "django.contrib.messages.middleware.MessageMiddleware", 75 | "django.middleware.clickjacking.XFrameOptionsMiddleware", 76 | ] 77 | 78 | ROOT_URLCONF = "config.urls" 79 | 80 | TEMPLATES = [ 81 | { 82 | "BACKEND": "django.template.backends.django.DjangoTemplates", 83 | "DIRS": [BASE_DIR / "templates"], 84 | "APP_DIRS": True, 85 | "OPTIONS": { 86 | "context_processors": [ 87 | "blog.context_processors.announcement", 88 | "payment.context_processors.get_payment_secret", 89 | "django.template.context_processors.debug", 90 | "django.template.context_processors.request", 91 | "django.contrib.auth.context_processors.auth", 92 | "django.contrib.messages.context_processors.messages", 93 | ], 94 | }, 95 | }, 96 | ] 97 | 98 | WSGI_APPLICATION = "config.wsgi.application" 99 | 100 | 101 | # Database 102 | # https://docs.djangoproject.com/en/3.2/ref/settings/#databases 103 | DATABASES = { 104 | "default": { 105 | "ENGINE": "django.db.backends.postgresql", 106 | "NAME": "church", 107 | "USER": "postgres", 108 | "PASSWORD": config("DB_PASSWORD"), 109 | "HOST": "localhost", 110 | "PORT": 5432, 111 | } 112 | } 113 | 114 | # Password validation 115 | # https://docs.djangoproject.com/en/3.2/ref/settings/#auth-password-validators 116 | 117 | AUTH_PASSWORD_VALIDATORS = [ 118 | { 119 | "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", 120 | }, 121 | { 122 | "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", 123 | }, 124 | { 125 | "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", 126 | }, 127 | { 128 | "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", 129 | }, 130 | ] 131 | 132 | 133 | # Internationalization 134 | # https://docs.djangoproject.com/en/3.2/topics/i18n/ 135 | 136 | LANGUAGE_CODE = "en-us" 137 | 138 | TIME_ZONE = "Africa/Lagos" 139 | 140 | USE_I18N = True 141 | 142 | USE_L10N = True 143 | 144 | USE_TZ = True 145 | 146 | 147 | # Static files (CSS, JavaScript, Images) 148 | # https://docs.djangoproject.com/en/3.2/howto/static-files/ 149 | 150 | STATIC_URL = "/static/" 151 | STATICFILES_DIRS = [ 152 | os.path.join(BASE_DIR, "static"), 153 | ] 154 | STATIC_ROOT = os.path.join(BASE_DIR, "staticfiles/") 155 | 156 | 157 | MEDIA_URL = "/media/" 158 | # MEDIA_ROOT = os.path.join(BASE_DIR, 'media') 159 | FILE_UPLOAD_PERMISSIONS = 0o640 160 | 161 | # Default primary key field type 162 | # https://docs.djangoproject.com/en/3.2/ref/settings/#default-auto-field 163 | 164 | DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" 165 | LOGIN_REDIRECT_URL = "blog:index" 166 | LOGIN_URL = "user:login" 167 | 168 | # REDIS configuration 169 | REDIS_HOST = "localhost" 170 | REDIS_PORT = 6379 171 | REDIS_DB = 0 172 | 173 | # CELERY config 174 | CELERY_BROKER_URL = "redis://localhost:6379" 175 | CELERY_RESULT_BACKEND = "redis://localhost:6379" 176 | 177 | # payment 178 | PAYSTACK_SECRET = config("PAYSTACK_SECRET") 179 | PAYSTACK_PUBLIC = config("PAYSTACK_PUBLIC") 180 | 181 | # Email Config 182 | EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend" 183 | -------------------------------------------------------------------------------- /apps/blog/templates/blog/about.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% load static %} 3 | {% block current-page %}About{% endblock current-page %} 4 | 5 | 6 | {% block content %} 7 |
8 |
9 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. 10 | Praesent volutpat ex ut pharetra elementum. Pellentesque vel suscipit turpis. Lorem ipsum 11 | dolor sit amet, consectetur adipiscing elit. Etiam blandit vel turpis et bibendum. Proin pulvinar 12 | est in tincidunt dapibus. Integer posuere rutrum aliquam. Donec vitae euismod eros. Praesent euismod 13 | euismod purus, dictum egestas nisl sollicitudin nec. Mauris varius dui sed augue rutrum molestie. 14 | Curabitur fringilla ut odio eget vestibulum. Nulla tempus justo urna, luctus 15 | vestibulum ante semper sit amet. 16 | 17 | In blandit quam et nulla dictum, quis dictum velit rhoncus. Proin tristique ante id congue malesuada. 18 | Donec vel aliquet enim. In hac habitasse platea dictumst. Nullam eget tempus massa. 19 | Proin ut nulla leo. 20 | In in dui id lectus auctor commodo. Ut non orci vel dolor iaculis tristique. 21 | Aliquam vel metus pretium 22 | augue bibendum convallis. Etiam faucibus nulla eget pellentesque faucibus. Vestibulum 23 | ante ipsum primis 24 | in faucibus orci luctus et ultrices posuere cubilia curae; Donec 25 | nibh lacus, varius sit amet urna vitae, 26 | feugiat iaculis eros. Quisque urna sapien, euismod vitae convallis et, volutpat nec felis. 27 | Mauris sed 28 | aliquet ante. Cras odio augue, pharetra id orci sed, fermentum laoreet turpis. 29 | 30 | Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. 31 | Nunc venenatis molestie velit, et rutrum sem venenatis eu. Quisque aliquet, urna vel vulputate 32 | luctus, erat elit posuere nulla, non feugiat tortor dolor et sem. Donec varius nisl elit, ac luctus 33 | metus tempus non. Aliquam vel magna id nisl feugiat convallis vitae id libero. 34 | Nunc sapien sapien, 35 | elementum quis scelerisque dapibus, lacinia ut diam. Vivamus scelerisque urna a enim finibus sodales. 36 | Morbi semper pretium lorem, et rhoncus elit fringilla sed. Sed vel tortor tincidunt, convallis 37 | lectus eu, laoreet dui. 38 | 39 | Nullam quis elit nec quam sollicitudin lobortis at ut sapien. Nulla eu tellus nisl. Quisque faucibus 40 | orci vitae tempor rutrum. Donec fermentum sodales enim a volutpat. 41 | Pellentesque auctor in metus sed 42 | iaculis. Nunc ornare, nibh nec bibendum mollis, eros neque malesuada nisi, id pretium sem nibh i 43 | d ante. 44 | Donec finibus eu enim eget lacinia. Curabitur tincidunt, est vitae aliquet 45 | vulputate, nisl magna 46 | sagittis 47 | velit, vel sodales enim leo ac lectus. Donec vehicula suscipit velit, a maximus erat posuere euismod. 48 | Phasellus viverra ultricies nibh eget viverra. 49 | 50 | Aliquam consectetur felis et nisi mattis aliquet. Duis volutpat arcu nec mollis pulvinar. Pellentesque 51 | sed ante leo. Vivamus a pharetra libero. Suspendisse potenti. Nam 52 | vehicula porta ante, vel ullamcorper 53 | lacus blandit eu. Maecenas scelerisque augue commodo orci interdum 54 | pulvinar. Cras ultricies pretium mi 55 | consectetur mattis. Morbi pharetra tellus ante. In est risus, 56 | tincidunt sed orci at, feugiat accumsan 57 | dolor. Etiam pellentesque convallis nisl ac varius. Suspendisse in venenatis sapien. 58 | 59 | Etiam malesuada ut elit vitae dapibus. Aenean id sagittis ipsum, at lacinia velit. Curabitur venenatis 60 | malesuada nisl in porttitor. Maecenas placerat aliquam enim, at volutpat nisl posuere in. Vivamus 61 | sit amet magna et velit efficitur interdum ut gravida arcu. 62 | Suspendisse potenti. Curabitur dignissim 63 | velit eu dui blandit lacinia. 64 | 65 | Etiam viverra fermentum commodo. Nulla in tempor dui. Morbi vehicula vitae justo pulvinar iaculis. Sed nisi 66 | est, interdum tristique leo quis, ornare pellentesque turpis. Duis 67 | molestie lacus ac pretium molestie. 68 | Aenean quis nisl justo. Cras at ullamcorper massa. 69 | 70 | Aenean a semper elit. Nunc vel odio vitae metus eleifend maximus bibendum eu nunc. Curabitur blandit 71 | dui id massa finibus, id lobortis mauris euismod. Proin ullamcorper ex et orci vulputate, sit amet 72 | gravida purus rutrum. Cras efficitur purus lobortis pulvinar feugiat. Nulla feugiat eros eleifend convallis 73 | fermentum. Nam dignissim lectus in posuere ultricies. Curabitur posuere vitae diam eget ullamcorper. 74 | 75 | Nullam quis felis sed dui sodales sagittis. Mauris aliquet ornare facilisis. Quisque aliquet nisi in nisi 76 | bibendum, et vestibulum purus lobortis. Suspendisse dignissim arcu et purus placerat, quis commodo arcu mollis. 77 | Proin dignissim pharetra mauris, vel eleifend risus commodo viverra. Cras eget laoreet erat. Morbi consequat 78 | nunc a sagittis porttitor. Aenean lorem lorem, varius quis neque pharetra, condimentum euismod augue. 79 | 80 | Vestibulum ultrices hendrerit blandit. Maecenas varius erat ac imperdiet convallis. 81 | Aliquam quam nunc, luctus ac nibh eu, imperdiet laoreet diam. Integer vitae porttitor arcu. Nam ac aliquet 82 | velit. Vivamus lobortis ligula magna, sit amet euismod odio scelerisque in. Aenean cursus lectus quis faucibus 83 | eleifend. Quisque pellentesque gravida urna, sit amet pellentesque purus placerat id. Donec venenatis fermentum 84 | neque, 85 | eget lacinia mauris porta ac. Cras luctus purus sed pretium feugiat. 86 |
87 |
88 | {% endblock content %} -------------------------------------------------------------------------------- /apps/blog/views.py: -------------------------------------------------------------------------------- 1 | import redis 2 | from django.conf import settings 3 | from django.contrib import messages 4 | from django.contrib.auth.decorators import login_required 5 | from django.core.exceptions import PermissionDenied 6 | from django.core.paginator import Paginator 7 | from django.http import HttpResponseRedirect, JsonResponse 8 | from django.shortcuts import HttpResponse, get_object_or_404, redirect, render 9 | from django.urls import reverse 10 | 11 | from apps.blog.tasks import today_bible_verse 12 | 13 | from apps.blog.forms import CommentForm, PostForm 14 | from apps.blog.models import BibleVerse, Category, Comment, Post 15 | from apps.blog.newsletter import newsletter 16 | 17 | # connect to redis 18 | r = redis.Redis( 19 | host=settings.REDIS_HOST, port=settings.REDIS_PORT, db=settings.REDIS_DB 20 | ) 21 | 22 | 23 | def index(request): 24 | category = request.GET.get("category", "") 25 | if not category: 26 | all_posts = Post.objects.all() 27 | else: 28 | all_posts = Post.objects.filter(category__category_name=category) 29 | paginator = Paginator(all_posts, 3) # Show 3 posts per page. 30 | page_number = request.GET.get("page") 31 | page_obj = paginator.get_page(page_number) 32 | all_categories = Category.objects.all() 33 | 34 | # today's bible verse 35 | today_bible_verse_obj = BibleVerse.today.first() 36 | text = "Not loaded Yet" 37 | if r.get("today_bible_verse") is None: 38 | today_bible_verse.delay(today_bible_verse_obj.id) 39 | else: 40 | text = r.get("today_bible_verse").decode("utf-8") 41 | bv = {"bible_ref": str(today_bible_verse_obj), "text": text} 42 | context = { 43 | "all_posts": all_posts, 44 | "all_categories": all_categories, 45 | "page_obj": page_obj, 46 | "category": category, 47 | "bv": bv, 48 | } 49 | return render(request, "blog/index.html", context) 50 | 51 | 52 | def about(request): 53 | return render(request, "blog/about.html") 54 | 55 | 56 | def detail(request, id, slug): 57 | post = get_object_or_404(Post, id=id, slug=slug) 58 | comments = post.comment_set.all() 59 | if request.method != "POST": 60 | form = CommentForm() 61 | else: 62 | form = CommentForm(request.POST) 63 | # if user isn't logged in redirect the user 64 | # to login page, after successful login 65 | # user should be back at the comment page 66 | # more like using next 67 | if not request.user.is_authenticated: 68 | return redirect("/user/login/?next=" + request.path + "#comments") 69 | if form.is_valid(): 70 | instance = form.save(commit=False) 71 | instance.user = request.user 72 | instance.post = post 73 | instance.save() 74 | return HttpResponseRedirect( 75 | reverse("blog:detail", args=(post.id, post.slug)) 76 | ) 77 | context = {"post": post, "form": form, "comments": comments} 78 | return render(request, "blog/detail.html", context) 79 | 80 | 81 | @login_required(login_url="user:login") 82 | def create_post(request): 83 | if request.method != "POST": 84 | form = PostForm() 85 | else: 86 | form = PostForm(request.POST, request.FILES) 87 | if form.is_valid(): 88 | instance = form.save(commit=False) 89 | instance.author = request.user 90 | instance.save() 91 | messages.success(request, "Post Created Successfully.") 92 | return HttpResponseRedirect( 93 | reverse("blog:detail", args=(instance.id, instance.slug)) 94 | ) 95 | else: 96 | messages.error(request, "Error Creating the Post.") 97 | context = {"form": form} 98 | return render(request, "blog/post_new.html", context) 99 | 100 | 101 | def edit_post(request, id, slug): 102 | post = get_object_or_404(Post, id=id, slug=slug) 103 | if request.user != post.author: 104 | raise PermissionDenied() 105 | if request.method != "POST": 106 | form = PostForm(instance=post) 107 | else: 108 | form = PostForm(request.POST, request.FILES, instance=post) 109 | if form.is_valid(): 110 | form.save() 111 | messages.success(request, "Post Edited Successfully.") 112 | return HttpResponseRedirect( 113 | reverse("blog:detail", args=(post.id, post.slug)) 114 | ) 115 | else: 116 | messages.error(request, "Error Editing the Post.") 117 | context = {"form": form} 118 | return render(request, "blog/post_edit.html", context) 119 | 120 | 121 | def delete_post(request, id, slug): 122 | post = get_object_or_404(Post, id=id, slug=slug) 123 | if request.user != post.author: 124 | raise PermissionDenied() 125 | post.delete() 126 | messages.success(request, f"Post: {post.title} deleted successfully") 127 | return HttpResponseRedirect(reverse("blog:index")) 128 | 129 | 130 | def subscribe(request): 131 | if request.method == "POST": 132 | email = request.POST["Nemail"] 133 | response = newsletter.add_member(email_address=email, status="subscribed") 134 | if response == "added" or response == "updated": 135 | messages.success(request, "You have been added to our Newsletter plan.") 136 | elif response == "Member Exists": 137 | messages.error(request, f"{email} already exists in our newsletter") 138 | else: 139 | messages.error(request, "An error occured!") 140 | return HttpResponseRedirect(reverse("blog:index")) 141 | 142 | 143 | def unsubscribe(request, email_hash): 144 | response = newsletter.unsubscribe(email_hash) 145 | if response == "updated": 146 | return HttpResponse("You have been unsubscribed from our newsletter!") 147 | return JsonResponse(response, safe=False) 148 | 149 | 150 | def error_404(request, exception): 151 | return render(request, "blog/404.html") 152 | 153 | 154 | def error_403(request, exception): 155 | return render(request, "blog/403_csrf.html") 156 | -------------------------------------------------------------------------------- /apps/sermon/templates/sermon/index.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% load static %} 3 | {% block style %} {% endblock style %} 4 | {% block title %}Believe - Sermons {% endblock title %} 5 | 6 | {% block content %} 7 |
8 |
9 |
10 |
11 | Bible 12 |

Today's Sermon

13 |

God loves us all

14 |
15 | {% with today_sermon as ts %} 16 |
17 |
18 | sermon image 19 |
20 | 21 |
22 |

{{ ts.title }}

23 |
24 |

Sermon by: {{ ts.author.first_name }} {{ ts.author.last_name }}

25 |

Tags: {{ ts.tags.all|join:", " }}

26 |

Date: {{ ts.date_created|date:"l j F, Y" }}

27 |

View: {{ total_views }} View{{ total_views|pluralize }}

28 |
29 | 30 |
31 | {{ ts.summary }} 32 |
33 | 34 |
    35 |
  • 36 |
  • 37 |
  • 38 |
  • 39 |
  • 40 |
  • 41 |
42 |
43 | {% endwith %} 44 |
45 |
46 |
47 |
48 | 49 | 50 | 92 | 93 | 94 | 132 | {% endblock content %} -------------------------------------------------------------------------------- /templates/base.html: -------------------------------------------------------------------------------- 1 | {% load static %} 2 | {% load filters %} 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | {% block style %}{% endblock style %} 15 | {% block title %}Believe Church{% endblock title %} 16 | 17 | 18 | 19 | 20 | 21 |
22 |
23 |
24 | 25 | {% if announcement %} 26 |

27 | {{ announcement.event_name }}: 28 |

29 |
30 |

31 | Days 32 |

33 |

34 | Hours 35 |

36 |

37 | Minutes 38 |

39 |

40 | Seconds 41 |

42 |
43 | {% else %} 44 |

45 | No Upcoming Event. 46 |

47 | {% endif %} 48 | Send Donations 49 |
50 |
51 |
52 | 53 | 54 | 80 | 81 | 82 |
83 |
84 |
85 |

{{ request.resolver_match.url_name|title|pretty_ }}

86 |

Home/ {% block current-page %}{% endblock current-page %}

87 |
88 |
89 |
90 | 91 | {% if messages %} 92 | {% for message in messages %} 93 | 96 | {% endfor %} 97 | {% endif %} 98 | 99 | 100 | {% block content %} 101 | {% endblock content %} 102 | 103 | 104 | 119 | 120 | 121 |
122 |
123 |
124 |
125 |

Believe 126 | logo 127 |

128 |

Church template

129 | 135 |
136 | 137 | 157 | 158 |
159 |

Contact Us

160 |
    161 |
  • 162 | Address: 163 | 1481 Creekside Lane Avila Beach, 164 | CA 93424 165 |
  • 166 | 167 |
  • 168 | Phone: 169 | +53 345 7953 32453 170 |
      171 |
    • 172 | +53 345 7557 822112 173 |
    • 174 |
    175 |
  • 176 | 177 |
  • 178 | Email: 179 | yourmail@gmail.com 180 |
  • 181 |
182 |
183 |
184 |
185 |
186 | 187 | 188 |
189 |
190 |

191 | Copyright © All rights reserved 192 | | This template was cloned with ♥ by 193 | Bovage 194 |

195 |
196 |
197 | 198 | 212 | 213 | 214 | 215 | 216 | {% block js %} 217 | {% endblock js %} 218 | 219 | -------------------------------------------------------------------------------- /static/blog/css/base.css: -------------------------------------------------------------------------------- 1 | /* genrel rules */ 2 | 3 | :root { 4 | --main-margin: 35px; 5 | --red-text-bg: #eb4141; 6 | --grey-text: #7c7c7c; 7 | } 8 | 9 | ::selection { 10 | color: var(--red-text-bg); 11 | } 12 | 13 | * { 14 | box-sizing: border-box; 15 | padding: 0; 16 | margin: 0; 17 | } 18 | 19 | body { 20 | font-family: Arial; 21 | line-height: 1.5; 22 | } 23 | 24 | a { 25 | text-decoration: none; 26 | color: black; 27 | } 28 | 29 | a:hover { 30 | color: var(--red-text-bg); 31 | } 32 | 33 | ul { 34 | list-style: none; 35 | } 36 | 37 | h1 { 38 | color: #000000; 39 | font-weight: normal; 40 | font-size: 24px; 41 | } 42 | 43 | h2 { 44 | font-size: 24px; 45 | color: #000000; 46 | font-weight: 500; 47 | } 48 | 49 | h3 { 50 | display: inline-block; 51 | font-size: 18px; 52 | font-weight: normal; 53 | margin-bottom: 30px; 54 | } 55 | 56 | .container { 57 | max-width: 1200px; 58 | margin: auto; 59 | } 60 | 61 | .margin-container { 62 | margin: 0 var(--main-margin); 63 | } 64 | 65 | span.red-text { 66 | color: var(--red-text-bg); 67 | } 68 | 69 | li { 70 | color: #6b6b6b; 71 | } 72 | 73 | li > ul { 74 | padding-left: 55px; 75 | } 76 | 77 | li.inner-list { 78 | margin-bottom: 10px; 79 | } 80 | 81 | .center { 82 | display: flex; 83 | flex-flow: column nowrap; 84 | align-items: center; 85 | } 86 | 87 | input, textarea { 88 | border: 0; 89 | margin-bottom: 10px; 90 | background-color: #ededed; 91 | padding-left: 30px; 92 | font-style: italic; 93 | height: 45px; 94 | } 95 | 96 | textarea { 97 | margin-top: 20px; 98 | width: 100%; 99 | height: 150px; 100 | padding-top: 20px; 101 | resize: vertical; 102 | } 103 | 104 | input.action { 105 | cursor: pointer; 106 | font-style: normal; 107 | border-bottom: 3px solid yellow; 108 | text-align: center; 109 | color: #ffffff; 110 | background-color: #000000; 111 | padding: 0; 112 | width: 100px; 113 | } 114 | 115 | input:focus { 116 | outline: 0; 117 | font-style: normal; 118 | } 119 | 120 | .circle-img { 121 | width: 70px; 122 | height: 70px; 123 | border-radius: 50%; 124 | } 125 | 126 | .error-img { 127 | width: 100%; 128 | } 129 | 130 | .description { 131 | color: var(--grey-text); 132 | font-size: 16px; 133 | line-height: 1.5; 134 | font-weight: 500; 135 | font-family: 'Montserrat', sans-serif; 136 | } 137 | /* end general rules */ 138 | 139 | /* bootstrap rules */ 140 | .alert { 141 | position: relative; 142 | padding: .75rem 1.25rem; 143 | margin-bottom: 1rem; 144 | border: 1px solid transparent; 145 | border-radius: .25rem 146 | } 147 | 148 | .alert-success { 149 | color: #155724; 150 | background-color: #d4edda; 151 | border-color: #c3e6cb 152 | } 153 | 154 | .alert-info { 155 | color: #0c5460; 156 | background-color: #d1ecf1; 157 | border-color: #bee5eb 158 | } 159 | 160 | .alert-warning { 161 | color: #856404; 162 | background-color: #fff3cd; 163 | border-color: #ffeeba 164 | } 165 | 166 | .alert-danger { 167 | color: #721c24; 168 | background-color: #f8d7da; 169 | border-color: #f5c6cb 170 | } 171 | 172 | .error-text { 173 | color: var(--red-text-bg); 174 | } 175 | 176 | /* end bootstrap rules */ 177 | 178 | 179 | /* top-container rules */ 180 | .top-container { 181 | background-color: #000000; 182 | } 183 | 184 | .countdown-container { 185 | display: flex; 186 | flex-flow: row nowrap; 187 | justify-content: flex-start; 188 | align-items: center; 189 | height: 60px; 190 | font-size: 14px; 191 | margin: 0 var(--main-margin) 25px; 192 | } 193 | 194 | p.yellow-text { 195 | color: #ffd600; 196 | margin-right: 23px; 197 | } 198 | 199 | div.time { 200 | color: #ffffff; 201 | font-weight: 600; 202 | display: flex; 203 | width: 30%; 204 | justify-content: space-between; 205 | } 206 | 207 | span.units { 208 | color: #868687; 209 | } 210 | 211 | .btn { 212 | box-sizing: border-box; 213 | color: #ffffff; 214 | display: inline-block; 215 | height: 100%; 216 | width: 157px; 217 | background-color: var(--red-text-bg); 218 | padding: 21px; 219 | text-align: center; 220 | margin-left: auto; 221 | } 222 | 223 | .btn:hover { 224 | display: inline-block; 225 | } 226 | 227 | 228 | /* nav bar rules */ 229 | .nav { 230 | position: sticky; 231 | top: 0px; 232 | height: 100px; 233 | z-index: 10; 234 | background-color: #ffffff; 235 | margin-bottom: 15px; 236 | } 237 | 238 | nav { 239 | display: flex; 240 | flex-flow: row nowrap; 241 | justify-content: space-between; 242 | align-items: center; 243 | margin: 0 var(--main-margin); 244 | height: 100%; 245 | } 246 | 247 | p.left-nav-logo { 248 | color: #353535; 249 | font-size: 24px; 250 | } 251 | 252 | p.left-nav-logo > img { 253 | vertical-align: baseline; 254 | } 255 | 256 | .right-nav > img{ 257 | display: none; 258 | } 259 | 260 | .right-nav > a { 261 | margin: 0 35px 0 5px; 262 | font-size: 14px; 263 | } 264 | 265 | img.menu { 266 | height: 25px; 267 | width: 25px; 268 | cursor: pointer; 269 | display: none; 270 | z-index: 3; 271 | } 272 | 273 | .search { 274 | display: none; 275 | } 276 | 277 | /* shows the menu bar */ 278 | .right-nav.show { 279 | width: 100%; 280 | height: 100vh; 281 | } 282 | 283 | 284 | /* showcase section rules */ 285 | .showcase-container { 286 | background-image: url("../images/blog.jpg"); 287 | background-repeat: no-repeat; 288 | background-size: cover; 289 | height: 130px; 290 | margin-bottom: 100px; 291 | } 292 | 293 | .showcase { 294 | height: 100%; 295 | } 296 | 297 | .showcase-text { 298 | height: 100%; 299 | display: flex; 300 | justify-content: space-between; 301 | align-items: center; 302 | color: #ffffff; 303 | margin: 0 var(--main-margin); 304 | } 305 | 306 | .showcase-text > h1 { 307 | color: #ffffff; 308 | font-size: 30px; 309 | font-weight: 500; 310 | } 311 | 312 | .home { 313 | color: #ffffff; 314 | } 315 | 316 | .home:hover { 317 | color: #ffffff; 318 | } 319 | 320 | 321 | /* blog posts section rules */ 322 | .two-cols-container { 323 | display: grid; 324 | grid-template-columns: repeat(12, 1fr); 325 | grid-template-areas: 326 | "b b b b b b b b b a a a" 327 | ; 328 | grid-column-gap: 30px; 329 | margin: 0 var(--main-margin); 330 | font-family: Helvetica; 331 | } 332 | 333 | .blog-posts { 334 | grid-area: b; 335 | } 336 | 337 | .img-container { 338 | position: relative; 339 | } 340 | 341 | .date { 342 | display: inline-block; 343 | background-color: var(--red-text-bg); 344 | color: #ffffff; 345 | text-align: center; 346 | padding: 10px; 347 | position: absolute; 348 | top: 0; 349 | left: 0; 350 | } 351 | 352 | p.day { 353 | font-size: 30px; 354 | } 355 | 356 | p.date-text { 357 | font-size: 14px; 358 | } 359 | 360 | .blog-post img { 361 | width: 100%; 362 | } 363 | 364 | .heading { 365 | margin: 30px 0; 366 | } 367 | 368 | .subtitle { 369 | color: var(--red-text-bg); 370 | font-size: 12px; 371 | } 372 | 373 | 374 | .blog-post { 375 | margin-bottom: 100px; 376 | } 377 | 378 | a.read-more { 379 | font-style: italic; 380 | font-size: 14px; 381 | color: var(--red-text-bg); 382 | } 383 | 384 | .pagination { 385 | margin-bottom: 100px; 386 | } 387 | 388 | .page-link { 389 | font-size: 18px; 390 | color: #000000; 391 | } 392 | 393 | .current-page { 394 | color: var(--red-text-bg); 395 | border-bottom: 2px solid #ffd600;; 396 | } 397 | 398 | 399 | aside { 400 | grid-area: a; 401 | } 402 | 403 | aside h2 { 404 | margin-bottom: 20px; 405 | } 406 | 407 | .search-box { 408 | width: 100%; 409 | background-color: #ededed; 410 | margin-bottom: 100px; 411 | } 412 | 413 | .side-post { 414 | display: flex; 415 | margin-bottom: 20px; 416 | } 417 | 418 | .side-post img { 419 | margin-right: 20px; 420 | } 421 | 422 | time { 423 | width: 50%; 424 | color: var(--red-text-bg); 425 | font-size: 12px; 426 | } 427 | 428 | .side-post > div > p { 429 | font-size: 14px; 430 | } 431 | 432 | .category { 433 | color: var(--grey-text); 434 | display: flex; 435 | justify-content: space-between; 436 | line-height: 3; 437 | font-size: 14px; 438 | } 439 | 440 | .archives { 441 | color: var(--grey-text); 442 | line-height: 3; 443 | font-size: 14px; 444 | margin: 20px 0 50px; 445 | } 446 | 447 | .archives > h2 { 448 | margin-bottom: 5px; 449 | } 450 | 451 | blockquote { 452 | background-image: url("../images/sidebar_quote.jpg"); 453 | background-size: cover; 454 | color: #ffffff; 455 | width: 100%; 456 | padding: 60px 20px; 457 | text-align: center; 458 | font-style: italic; 459 | } 460 | 461 | span.bible-ref { 462 | display: inline-block; 463 | color: #e6c100; 464 | font-size: 18px; 465 | margin-top: 30px; 466 | } 467 | 468 | /* newsletter rules */ 469 | .newsletter-container { 470 | background-color: var(--red-text-bg); 471 | } 472 | 473 | .newsletter-wrapper { 474 | display: flex; 475 | height: 120px; 476 | flex-flow: row wrap; 477 | align-items: center; 478 | margin: 0 var(--main-margin); 479 | } 480 | 481 | .newsletter-wrapper > p { 482 | color: #ffffff; 483 | font-size: 36px; 484 | font-weight: 500; 485 | width: 50%; 486 | } 487 | 488 | #newsletter { 489 | width: 50%; 490 | } 491 | 492 | input.newsemail { 493 | border-bottom: 3px solid yellow; 494 | position: relative; 495 | left: 6px; 496 | width: 69%; 497 | margin: 0; 498 | } 499 | 500 | input.subscribe { 501 | margin: 0; 502 | height: 45px; 503 | width: 30%; 504 | } 505 | 506 | /* bottom-container rules */ 507 | .bottom-container { 508 | background-color: #161619; 509 | } 510 | 511 | .three-cols-container { 512 | display: grid; 513 | grid-template-columns: 1fr 1fr 1fr; 514 | grid-column-gap: 60px; 515 | padding: 100px 0; 516 | margin: 0 var(--main-margin); 517 | } 518 | 519 | .three-cols-container h3 { 520 | border-bottom: 2px solid #eb4141; 521 | } 522 | 523 | .social-icons { 524 | margin: 40px 0; 525 | } 526 | 527 | .social-icons > i { 528 | color: var(--red-text-bg); 529 | font-size: 18px; 530 | margin-right: 20px; 531 | } 532 | 533 | .social-icons > i:hover { 534 | color: white; 535 | } 536 | 537 | h1.logo-name { 538 | font-size: 49px; 539 | color: #ffffff; 540 | } 541 | 542 | .logo-name + p { 543 | color: #797979; 544 | font-size: 14px; 545 | } 546 | 547 | .three-cols-container h3 { 548 | color: #ffffff; 549 | } 550 | 551 | .links-wrapper { 552 | display: flex; 553 | justify-content: space-between; 554 | } 555 | 556 | .links-wrapper li { 557 | margin-bottom: 20px; 558 | } 559 | 560 | .links-wrapper a { 561 | color: #797979; 562 | font-weight: 600; 563 | } 564 | 565 | .links-wrapper a:hover { 566 | color: var(--red-text-bg); 567 | } 568 | 569 | /* footer rules */ 570 | footer { 571 | background-color: #0d0d0f; 572 | padding: 35px 0; 573 | } 574 | 575 | footer p { 576 | font-size: 13px; 577 | color: #343439; 578 | text-align: center; 579 | } 580 | 581 | footer a { 582 | color: blue; 583 | } 584 | 585 | /* detail-post rules */ 586 | .detail-post { 587 | margin: 0 var(--main-margin); 588 | } 589 | 590 | .detail-post-img { 591 | width: 100%; 592 | } 593 | 594 | .comment { 595 | border: 2px solid var(--grey-text); 596 | border-radius: 8px; 597 | padding: 10px; 598 | margin-bottom: 10px; 599 | } 600 | 601 | .comment-details { 602 | display: flex; 603 | flex-flow: row wrap; 604 | } 605 | 606 | .comment-date { 607 | margin-left: auto; 608 | } 609 | /* end detail-post rules */ 610 | 611 | 612 | /* media query */ 613 | @media screen and (max-width: 990px) { 614 | .right-nav { 615 | overflow: hidden; 616 | display: flex; 617 | flex-flow: column wrap; 618 | position: absolute; 619 | top: 0; 620 | left: 0; 621 | background-color: rgba(255,255,255,0.98); 622 | width: 0; 623 | height: 0; 624 | text-align: center; 625 | transition: height 0.12s ease-in-out, width 0.12s ease-in-out; 626 | } 627 | 628 | .right-nav > img{ 629 | width: 50px; 630 | height: 50px; 631 | display: block; 632 | background-color: var(--red-text-bg); 633 | border-radius: 50%; 634 | padding: 20px; 635 | margin: 25px auto 10px; 636 | } 637 | 638 | .search { 639 | display: inline-block; 640 | font-style: normal; 641 | background-color: transparent; 642 | width: 90%; 643 | margin-bottom: 25px; 644 | } 645 | 646 | .right-nav a { 647 | display: inline-block; 648 | width: 100%; 649 | text-align: center; 650 | font-size: 20px; 651 | font-weight: bold; 652 | color: #353535; 653 | margin: 0; 654 | } 655 | 656 | .right-nav a:hover { 657 | color: var(--red-text-bg); 658 | } 659 | 660 | img.menu { 661 | display: block; 662 | } 663 | 664 | .countdown-container, 665 | nav, .showcase-text, 666 | .two-cols-container, 667 | .contact-us, 668 | .newsletter-wrapper, 669 | .detail-post, 670 | .margin-container, 671 | .three-cols-container { 672 | width: 75%; 673 | margin-left: auto; 674 | margin-right: auto; 675 | } 676 | 677 | .two-cols-container { 678 | grid-template-rows: auto auto; 679 | grid-template-areas: 680 | "b b b b b b b b b b b b" 681 | "a a a a a a a a a a a a" 682 | ; 683 | } 684 | 685 | aside { 686 | margin-bottom: 100px; 687 | } 688 | 689 | .newsletter-wrapper { 690 | flex-flow: column wrap; 691 | height: 250px; 692 | justify-content: center; 693 | } 694 | 695 | .newsletter-wrapper > p { 696 | width: 100%; 697 | text-align: center; 698 | margin-bottom: 30px; 699 | } 700 | 701 | form#newsletter { 702 | width: 100%; 703 | text-align: center; 704 | } 705 | 706 | input.newsemail { 707 | width: 67%; 708 | } 709 | 710 | input.subscribe { 711 | max-width: 30%; 712 | } 713 | 714 | .three-cols-container { 715 | grid-template-columns: 1fr; 716 | } 717 | 718 | .description, 719 | .links-container { 720 | margin-bottom: 50px; 721 | } 722 | 723 | .link-wrapper:first-child { 724 | margin-right: auto; 725 | } 726 | 727 | .link-wrapper:last-child { 728 | margin-right: auto; 729 | } 730 | } 731 | 732 | @media screen and (max-width: 768px) { 733 | .countdown-container { 734 | display: none; 735 | } 736 | 737 | .newsletter-wrapper > p { 738 | font-size: 30px; 739 | width: 100%; 740 | } 741 | } 742 | 743 | @media screen and (max-width: 580px) { 744 | .countdown-container, 745 | nav, .showcase-text, 746 | .two-cols-container, 747 | .contact-us, 748 | .newsletter-wrapper, 749 | .detail-post, 750 | .three-cols-container { 751 | width: 95%; 752 | margin-left: auto; 753 | margin-right: auto; 754 | } 755 | 756 | .newsletter-wrapper > p { 757 | font-size: 24px; 758 | width: 100%; 759 | } 760 | } -------------------------------------------------------------------------------- /static/admin/css/base.css: -------------------------------------------------------------------------------- 1 | /* 2 | DJANGO Admin styles 3 | */ 4 | 5 | @import url(fonts.css); 6 | 7 | /* VARIABLE DEFINITIONS */ 8 | :root { 9 | --primary: #79aec8; 10 | --secondary: #417690; 11 | --accent: #f5dd5d; 12 | --primary-fg: #fff; 13 | 14 | --body-fg: #333; 15 | --body-bg: #fff; 16 | --body-quiet-color: #666; 17 | --body-loud-color: #000; 18 | 19 | --header-color: #ffc; 20 | --header-branding-color: var(--accent); 21 | --header-bg: var(--secondary); 22 | --header-link-color: var(--primary-fg); 23 | 24 | --breadcrumbs-fg: #c4dce8; 25 | --breadcrumbs-link-fg: var(--body-bg); 26 | --breadcrumbs-bg: var(--primary); 27 | 28 | --link-fg: #447e9b; 29 | --link-hover-color: #036; 30 | --link-selected-fg: #5b80b2; 31 | 32 | --hairline-color: #e8e8e8; 33 | --border-color: #ccc; 34 | 35 | --error-fg: #ba2121; 36 | 37 | --message-success-bg: #dfd; 38 | --message-warning-bg: #ffc; 39 | --message-error-bg: #ffefef; 40 | 41 | --darkened-bg: #f8f8f8; /* A bit darker than --body-bg */ 42 | --selected-bg: #e4e4e4; /* E.g. selected table cells */ 43 | --selected-row: #ffc; 44 | 45 | --button-fg: #fff; 46 | --button-bg: var(--primary); 47 | --button-hover-bg: #609ab6; 48 | --default-button-bg: var(--secondary); 49 | --default-button-hover-bg: #205067; 50 | --close-button-bg: #888; /* Previously #bbb, contrast 1.92 */ 51 | --close-button-hover-bg: #747474; 52 | --delete-button-bg: #ba2121; 53 | --delete-button-hover-bg: #a41515; 54 | 55 | --object-tools-fg: var(--button-fg); 56 | --object-tools-bg: var(--close-button-bg); 57 | --object-tools-hover-bg: var(--close-button-hover-bg); 58 | } 59 | 60 | @media (prefers-color-scheme: dark) { 61 | :root { 62 | --primary: #264b5d; 63 | --primary-fg: #eee; 64 | 65 | --body-fg: #eeeeee; 66 | --body-bg: #121212; 67 | --body-quiet-color: #e0e0e0; 68 | --body-loud-color: #ffffff; 69 | 70 | --breadcrumbs-link-fg: #e0e0e0; 71 | --breadcrumbs-bg: var(--primary); 72 | 73 | --link-fg: #81d4fa; 74 | --link-hover-color: #4ac1f7; 75 | --link-selected-fg: #6f94c6; 76 | 77 | --hairline-color: #272727; 78 | --border-color: #353535; 79 | 80 | --error-fg: #e35f5f; 81 | --message-success-bg: #006b1b; 82 | --message-warning-bg: #583305; 83 | --message-error-bg: #570808; 84 | 85 | --darkened-bg: #212121; 86 | --selected-bg: #1b1b1b; 87 | --selected-row: #00363a; 88 | 89 | --close-button-bg: #333333; 90 | --close-button-hover-bg: #666666; 91 | } 92 | } 93 | 94 | html, body { 95 | height: 100%; 96 | } 97 | 98 | body { 99 | margin: 0; 100 | padding: 0; 101 | font-size: 14px; 102 | font-family: "Roboto","Lucida Grande","DejaVu Sans","Bitstream Vera Sans",Verdana,Arial,sans-serif; 103 | color: var(--body-fg); 104 | background: var(--body-bg); 105 | } 106 | 107 | /* LINKS */ 108 | 109 | a:link, a:visited { 110 | color: var(--link-fg); 111 | text-decoration: none; 112 | transition: color 0.15s, background 0.15s; 113 | } 114 | 115 | a:focus, a:hover { 116 | color: var(--link-hover-color); 117 | } 118 | 119 | a:focus { 120 | text-decoration: underline; 121 | } 122 | 123 | a img { 124 | border: none; 125 | } 126 | 127 | a.section:link, a.section:visited { 128 | color: var(--header-link-color); 129 | text-decoration: none; 130 | } 131 | 132 | a.section:focus, a.section:hover { 133 | text-decoration: underline; 134 | } 135 | 136 | /* GLOBAL DEFAULTS */ 137 | 138 | p, ol, ul, dl { 139 | margin: .2em 0 .8em 0; 140 | } 141 | 142 | p { 143 | padding: 0; 144 | line-height: 140%; 145 | } 146 | 147 | h1,h2,h3,h4,h5 { 148 | font-weight: bold; 149 | } 150 | 151 | h1 { 152 | margin: 0 0 20px; 153 | font-weight: 300; 154 | font-size: 20px; 155 | color: var(--body-quiet-color); 156 | } 157 | 158 | h2 { 159 | font-size: 16px; 160 | margin: 1em 0 .5em 0; 161 | } 162 | 163 | h2.subhead { 164 | font-weight: normal; 165 | margin-top: 0; 166 | } 167 | 168 | h3 { 169 | font-size: 14px; 170 | margin: .8em 0 .3em 0; 171 | color: var(--body-quiet-color); 172 | font-weight: bold; 173 | } 174 | 175 | h4 { 176 | font-size: 12px; 177 | margin: 1em 0 .8em 0; 178 | padding-bottom: 3px; 179 | } 180 | 181 | h5 { 182 | font-size: 10px; 183 | margin: 1.5em 0 .5em 0; 184 | color: var(--body-quiet-color); 185 | text-transform: uppercase; 186 | letter-spacing: 1px; 187 | } 188 | 189 | ul > li { 190 | list-style-type: square; 191 | padding: 1px 0; 192 | } 193 | 194 | li ul { 195 | margin-bottom: 0; 196 | } 197 | 198 | li, dt, dd { 199 | font-size: 13px; 200 | line-height: 20px; 201 | } 202 | 203 | dt { 204 | font-weight: bold; 205 | margin-top: 4px; 206 | } 207 | 208 | dd { 209 | margin-left: 0; 210 | } 211 | 212 | form { 213 | margin: 0; 214 | padding: 0; 215 | } 216 | 217 | fieldset { 218 | margin: 0; 219 | min-width: 0; 220 | padding: 0; 221 | border: none; 222 | border-top: 1px solid var(--hairline-color); 223 | } 224 | 225 | blockquote { 226 | font-size: 11px; 227 | color: #777; 228 | margin-left: 2px; 229 | padding-left: 10px; 230 | border-left: 5px solid #ddd; 231 | } 232 | 233 | code, pre { 234 | font-family: "Bitstream Vera Sans Mono", Monaco, "Courier New", Courier, monospace; 235 | color: var(--body-quiet-color); 236 | font-size: 12px; 237 | overflow-x: auto; 238 | } 239 | 240 | pre.literal-block { 241 | margin: 10px; 242 | background: var(--darkened-bg); 243 | padding: 6px 8px; 244 | } 245 | 246 | code strong { 247 | color: #930; 248 | } 249 | 250 | hr { 251 | clear: both; 252 | color: var(--hairline-color); 253 | background-color: var(--hairline-color); 254 | height: 1px; 255 | border: none; 256 | margin: 0; 257 | padding: 0; 258 | font-size: 1px; 259 | line-height: 1px; 260 | } 261 | 262 | /* TEXT STYLES & MODIFIERS */ 263 | 264 | .small { 265 | font-size: 11px; 266 | } 267 | 268 | .mini { 269 | font-size: 10px; 270 | } 271 | 272 | .help, p.help, form p.help, div.help, form div.help, div.help li { 273 | font-size: 11px; 274 | color: var(--body-quiet-color); 275 | } 276 | 277 | div.help ul { 278 | margin-bottom: 0; 279 | } 280 | 281 | .help-tooltip { 282 | cursor: help; 283 | } 284 | 285 | p img, h1 img, h2 img, h3 img, h4 img, td img { 286 | vertical-align: middle; 287 | } 288 | 289 | .quiet, a.quiet:link, a.quiet:visited { 290 | color: var(--body-quiet-color); 291 | font-weight: normal; 292 | } 293 | 294 | .clear { 295 | clear: both; 296 | } 297 | 298 | .nowrap { 299 | white-space: nowrap; 300 | } 301 | 302 | .hidden { 303 | display: none; 304 | } 305 | 306 | /* TABLES */ 307 | 308 | table { 309 | border-collapse: collapse; 310 | border-color: var(--border-color); 311 | } 312 | 313 | td, th { 314 | font-size: 13px; 315 | line-height: 16px; 316 | border-bottom: 1px solid var(--hairline-color); 317 | vertical-align: top; 318 | padding: 8px; 319 | } 320 | 321 | th { 322 | font-weight: 600; 323 | text-align: left; 324 | } 325 | 326 | thead th, 327 | tfoot td { 328 | color: var(--body-quiet-color); 329 | padding: 5px 10px; 330 | font-size: 11px; 331 | background: var(--body-bg); 332 | border: none; 333 | border-top: 1px solid var(--hairline-color); 334 | border-bottom: 1px solid var(--hairline-color); 335 | } 336 | 337 | tfoot td { 338 | border-bottom: none; 339 | border-top: 1px solid var(--hairline-color); 340 | } 341 | 342 | thead th.required { 343 | color: var(--body-loud-color); 344 | } 345 | 346 | tr.alt { 347 | background: var(--darkened-bg); 348 | } 349 | 350 | tr:nth-child(odd), .row-form-errors { 351 | background: var(--body-bg); 352 | } 353 | 354 | tr:nth-child(even), 355 | tr:nth-child(even) .errorlist, 356 | tr:nth-child(odd) + .row-form-errors, 357 | tr:nth-child(odd) + .row-form-errors .errorlist { 358 | background: var(--darkened-bg); 359 | } 360 | 361 | /* SORTABLE TABLES */ 362 | 363 | thead th { 364 | padding: 5px 10px; 365 | line-height: normal; 366 | text-transform: uppercase; 367 | background: var(--darkened-bg); 368 | } 369 | 370 | thead th a:link, thead th a:visited { 371 | color: var(--body-quiet-color); 372 | } 373 | 374 | thead th.sorted { 375 | background: var(--selected-bg); 376 | } 377 | 378 | thead th.sorted .text { 379 | padding-right: 42px; 380 | } 381 | 382 | table thead th .text span { 383 | padding: 8px 10px; 384 | display: block; 385 | } 386 | 387 | table thead th .text a { 388 | display: block; 389 | cursor: pointer; 390 | padding: 8px 10px; 391 | } 392 | 393 | table thead th .text a:focus, table thead th .text a:hover { 394 | background: var(--selected-bg); 395 | } 396 | 397 | thead th.sorted a.sortremove { 398 | visibility: hidden; 399 | } 400 | 401 | table thead th.sorted:hover a.sortremove { 402 | visibility: visible; 403 | } 404 | 405 | table thead th.sorted .sortoptions { 406 | display: block; 407 | padding: 9px 5px 0 5px; 408 | float: right; 409 | text-align: right; 410 | } 411 | 412 | table thead th.sorted .sortpriority { 413 | font-size: .8em; 414 | min-width: 12px; 415 | text-align: center; 416 | vertical-align: 3px; 417 | margin-left: 2px; 418 | margin-right: 2px; 419 | } 420 | 421 | table thead th.sorted .sortoptions a { 422 | position: relative; 423 | width: 14px; 424 | height: 14px; 425 | display: inline-block; 426 | background: url(../img/sorting-icons.svg) 0 0 no-repeat; 427 | background-size: 14px auto; 428 | } 429 | 430 | table thead th.sorted .sortoptions a.sortremove { 431 | background-position: 0 0; 432 | } 433 | 434 | table thead th.sorted .sortoptions a.sortremove:after { 435 | content: '\\'; 436 | position: absolute; 437 | top: -6px; 438 | left: 3px; 439 | font-weight: 200; 440 | font-size: 18px; 441 | color: var(--body-quiet-color); 442 | } 443 | 444 | table thead th.sorted .sortoptions a.sortremove:focus:after, 445 | table thead th.sorted .sortoptions a.sortremove:hover:after { 446 | color: var(--link-fg); 447 | } 448 | 449 | table thead th.sorted .sortoptions a.sortremove:focus, 450 | table thead th.sorted .sortoptions a.sortremove:hover { 451 | background-position: 0 -14px; 452 | } 453 | 454 | table thead th.sorted .sortoptions a.ascending { 455 | background-position: 0 -28px; 456 | } 457 | 458 | table thead th.sorted .sortoptions a.ascending:focus, 459 | table thead th.sorted .sortoptions a.ascending:hover { 460 | background-position: 0 -42px; 461 | } 462 | 463 | table thead th.sorted .sortoptions a.descending { 464 | top: 1px; 465 | background-position: 0 -56px; 466 | } 467 | 468 | table thead th.sorted .sortoptions a.descending:focus, 469 | table thead th.sorted .sortoptions a.descending:hover { 470 | background-position: 0 -70px; 471 | } 472 | 473 | /* FORM DEFAULTS */ 474 | 475 | input, textarea, select, .form-row p, form .button { 476 | margin: 2px 0; 477 | padding: 2px 3px; 478 | vertical-align: middle; 479 | font-family: "Roboto", "Lucida Grande", Verdana, Arial, sans-serif; 480 | font-weight: normal; 481 | font-size: 13px; 482 | } 483 | .form-row div.help { 484 | padding: 2px 3px; 485 | } 486 | 487 | textarea { 488 | vertical-align: top; 489 | } 490 | 491 | input[type=text], input[type=password], input[type=email], input[type=url], 492 | input[type=number], input[type=tel], textarea, select, .vTextField { 493 | border: 1px solid var(--border-color); 494 | border-radius: 4px; 495 | padding: 5px 6px; 496 | margin-top: 0; 497 | color: var(--body-fg); 498 | background-color: var(--body-bg); 499 | } 500 | 501 | input[type=text]:focus, input[type=password]:focus, input[type=email]:focus, 502 | input[type=url]:focus, input[type=number]:focus, input[type=tel]:focus, 503 | textarea:focus, select:focus, .vTextField:focus { 504 | border-color: var(--body-quiet-color); 505 | } 506 | 507 | select { 508 | height: 30px; 509 | } 510 | 511 | select[multiple] { 512 | /* Allow HTML size attribute to override the height in the rule above. */ 513 | height: auto; 514 | min-height: 150px; 515 | } 516 | 517 | /* FORM BUTTONS */ 518 | 519 | .button, input[type=submit], input[type=button], .submit-row input, a.button { 520 | cursor: pointer; 521 | font-style: normal; 522 | border-bottom: 3px solid yellow; 523 | text-align: center; 524 | color: #ffffff; 525 | background-color: #000000; 526 | padding: 10px 15px; 527 | } 528 | 529 | a.button { 530 | padding: 4px 5px; 531 | } 532 | 533 | .button:active, input[type=submit]:active, input[type=button]:active, 534 | .button:focus, input[type=submit]:focus, input[type=button]:focus, 535 | .button:hover, input[type=submit]:hover, input[type=button]:hover { 536 | background: #fff; 537 | color: #000; 538 | } 539 | 540 | .button[disabled], input[type=submit][disabled], input[type=button][disabled] { 541 | opacity: 0.4; 542 | } 543 | 544 | .button.default, input[type=submit].default, .submit-row input.default { 545 | float: right; 546 | cursor: pointer; 547 | font-style: normal; 548 | border-bottom: 3px solid yellow; 549 | text-align: center; 550 | color: #ffffff; 551 | background-color: #000000; 552 | padding: 10px 15px; 553 | } 554 | 555 | .button.default:active, input[type=submit].default:active, 556 | .button.default:focus, input[type=submit].default:focus, 557 | .button.default:hover, input[type=submit].default:hover { 558 | background: #fff; 559 | color: #000; 560 | } 561 | 562 | .button[disabled].default, 563 | input[type=submit][disabled].default, 564 | input[type=button][disabled].default { 565 | opacity: 0.4; 566 | } 567 | 568 | 569 | /* MODULES */ 570 | 571 | .module { 572 | border: none; 573 | margin-bottom: 30px; 574 | background: var(--body-bg); 575 | } 576 | 577 | .module p, .module ul, .module h3, .module h4, .module dl, .module pre { 578 | padding-left: 10px; 579 | padding-right: 10px; 580 | } 581 | 582 | .module blockquote { 583 | margin-left: 12px; 584 | } 585 | 586 | .module ul, .module ol { 587 | margin-left: 1.5em; 588 | } 589 | 590 | .module h3 { 591 | margin-top: .6em; 592 | } 593 | 594 | .module h2, .module caption, .inline-group h2 { 595 | margin: 0; 596 | padding: 8px; 597 | font-weight: 400; 598 | font-size: 13px; 599 | text-align: left; 600 | background: #000; 601 | color: var(--header-link-color); 602 | } 603 | 604 | .module caption, 605 | .inline-group h2 { 606 | font-size: 12px; 607 | letter-spacing: 0.5px; 608 | text-transform: uppercase; 609 | } 610 | 611 | .module table { 612 | border-collapse: collapse; 613 | } 614 | 615 | /* MESSAGES & ERRORS */ 616 | 617 | ul.messagelist { 618 | padding: 0; 619 | margin: 0; 620 | } 621 | 622 | ul.messagelist li { 623 | display: block; 624 | font-weight: 400; 625 | font-size: 13px; 626 | padding: 10px 10px 10px 65px; 627 | margin: 0 0 10px 0; 628 | background: var(--message-success-bg) url(../img/icon-yes.svg) 40px 12px no-repeat; 629 | background-size: 16px auto; 630 | color: var(--body-fg); 631 | } 632 | 633 | ul.messagelist li.warning { 634 | background: var(--message-warning-bg) url(../img/icon-alert.svg) 40px 14px no-repeat; 635 | background-size: 14px auto; 636 | } 637 | 638 | ul.messagelist li.error { 639 | background: var(--message-error-bg) url(../img/icon-no.svg) 40px 12px no-repeat; 640 | background-size: 16px auto; 641 | } 642 | 643 | .errornote { 644 | font-size: 14px; 645 | font-weight: 700; 646 | display: block; 647 | padding: 10px 12px; 648 | margin: 0 0 10px 0; 649 | color: var(--error-fg); 650 | border: 1px solid var(--error-fg); 651 | border-radius: 4px; 652 | background-color: var(--body-bg); 653 | background-position: 5px 12px; 654 | overflow-wrap: break-word; 655 | } 656 | 657 | ul.errorlist { 658 | margin: 0 0 4px; 659 | padding: 0; 660 | color: var(--error-fg); 661 | background: var(--body-bg); 662 | } 663 | 664 | ul.errorlist li { 665 | font-size: 13px; 666 | display: block; 667 | margin-bottom: 4px; 668 | overflow-wrap: break-word; 669 | } 670 | 671 | ul.errorlist li:first-child { 672 | margin-top: 0; 673 | } 674 | 675 | ul.errorlist li a { 676 | color: inherit; 677 | text-decoration: underline; 678 | } 679 | 680 | td ul.errorlist { 681 | margin: 0; 682 | padding: 0; 683 | } 684 | 685 | td ul.errorlist li { 686 | margin: 0; 687 | } 688 | 689 | .form-row.errors { 690 | margin: 0; 691 | border: none; 692 | border-bottom: 1px solid var(--hairline-color); 693 | background: none; 694 | } 695 | 696 | .form-row.errors ul.errorlist li { 697 | padding-left: 0; 698 | } 699 | 700 | .errors input, .errors select, .errors textarea, 701 | td ul.errorlist + input, td ul.errorlist + select, td ul.errorlist + textarea { 702 | border: 1px solid var(--error-fg); 703 | } 704 | 705 | .description { 706 | font-size: 12px; 707 | padding: 5px 0 0 12px; 708 | } 709 | 710 | /* BREADCRUMBS */ 711 | 712 | div.breadcrumbs { 713 | background: var(--breadcrumbs-bg); 714 | padding: 10px 40px; 715 | border: none; 716 | color: var(--breadcrumbs-fg); 717 | text-align: left; 718 | } 719 | 720 | div.breadcrumbs a { 721 | color: var(--breadcrumbs-link-fg); 722 | } 723 | 724 | div.breadcrumbs a:focus, div.breadcrumbs a:hover { 725 | color: var(--breadcrumbs-fg); 726 | } 727 | 728 | /* ACTION ICONS */ 729 | 730 | .viewlink, .inlineviewlink { 731 | padding-left: 16px; 732 | background: url(../img/icon-viewlink.svg) 0 1px no-repeat; 733 | } 734 | 735 | .addlink { 736 | padding-left: 16px; 737 | background: url(../img/icon-addlink.svg) 0 1px no-repeat; 738 | } 739 | 740 | .changelink, .inlinechangelink { 741 | padding-left: 16px; 742 | background: url(../img/icon-changelink.svg) 0 1px no-repeat; 743 | } 744 | 745 | .deletelink { 746 | padding-left: 16px; 747 | background: url(../img/icon-deletelink.svg) 0 1px no-repeat; 748 | } 749 | 750 | a.deletelink:link, a.deletelink:visited { 751 | color: #CC3434; /* XXX Probably unused? */ 752 | } 753 | 754 | a.deletelink:focus, a.deletelink:hover { 755 | color: #993333; /* XXX Probably unused? */ 756 | text-decoration: none; 757 | } 758 | 759 | /* OBJECT TOOLS */ 760 | 761 | .object-tools { 762 | font-size: 10px; 763 | font-weight: bold; 764 | padding-left: 0; 765 | float: right; 766 | position: relative; 767 | margin-top: -48px; 768 | } 769 | 770 | .object-tools li { 771 | display: block; 772 | float: left; 773 | margin-left: 5px; 774 | height: 16px; 775 | } 776 | 777 | .object-tools a { 778 | border-radius: 15px; 779 | } 780 | 781 | .object-tools a:link, .object-tools a:visited { 782 | display: block; 783 | float: left; 784 | padding: 3px 12px; 785 | background: var(--object-tools-bg); 786 | color: var(--object-tools-fg); 787 | font-weight: 400; 788 | font-size: 11px; 789 | text-transform: uppercase; 790 | letter-spacing: 0.5px; 791 | } 792 | 793 | .object-tools a:focus, .object-tools a:hover { 794 | background-color: var(--object-tools-hover-bg); 795 | } 796 | 797 | .object-tools a:focus{ 798 | text-decoration: none; 799 | } 800 | 801 | .object-tools a.viewsitelink, .object-tools a.addlink { 802 | background-repeat: no-repeat; 803 | background-position: right 7px center; 804 | padding-right: 26px; 805 | } 806 | 807 | .object-tools a.viewsitelink { 808 | background-image: url(../img/tooltag-arrowright.svg); 809 | } 810 | 811 | .object-tools a.addlink { 812 | background-image: url(../img/tooltag-add.svg); 813 | } 814 | 815 | /* OBJECT HISTORY */ 816 | 817 | table#change-history { 818 | width: 100%; 819 | } 820 | 821 | table#change-history tbody th { 822 | width: 16em; 823 | } 824 | 825 | /* PAGE STRUCTURE */ 826 | 827 | #container { 828 | position: relative; 829 | width: 100%; 830 | min-width: 980px; 831 | padding: 0; 832 | display: flex; 833 | flex-direction: column; 834 | height: 100%; 835 | } 836 | 837 | #container > div { 838 | flex-shrink: 0; 839 | } 840 | 841 | #container > .main { 842 | display: flex; 843 | flex: 1 0 auto; 844 | } 845 | 846 | .main > .content { 847 | flex: 1 0; 848 | max-width: 100%; 849 | } 850 | 851 | #content { 852 | padding: 20px 40px; 853 | } 854 | 855 | .dashboard #content { 856 | width: 600px; 857 | } 858 | 859 | #content-main { 860 | float: left; 861 | width: 100%; 862 | } 863 | 864 | #content-related { 865 | float: right; 866 | width: 260px; 867 | position: relative; 868 | margin-right: -300px; 869 | } 870 | 871 | #footer { 872 | clear: both; 873 | padding: 10px; 874 | } 875 | 876 | /* COLUMN TYPES */ 877 | 878 | .colMS { 879 | margin-right: 300px; 880 | } 881 | 882 | .colSM { 883 | margin-left: 300px; 884 | } 885 | 886 | .colSM #content-related { 887 | float: left; 888 | margin-right: 0; 889 | margin-left: -300px; 890 | } 891 | 892 | .colSM #content-main { 893 | float: right; 894 | } 895 | 896 | .popup .colM { 897 | width: auto; 898 | } 899 | 900 | /* HEADER */ 901 | 902 | #header { 903 | width: auto; 904 | height: auto; 905 | display: flex; 906 | justify-content: space-between; 907 | align-items: center; 908 | padding: 10px 40px; 909 | background: #000; 910 | color: var(--header-color); 911 | overflow: hidden; 912 | } 913 | 914 | #header a:link, #header a:visited { 915 | color: var(--header-link-color); 916 | } 917 | 918 | #header a:focus , #header a:hover { 919 | text-decoration: underline; 920 | } 921 | 922 | #branding { 923 | float: left; 924 | } 925 | 926 | #branding h1 { 927 | padding: 0; 928 | margin: 0 20px 0 0; 929 | font-weight: 300; 930 | font-size: 24px; 931 | color: var(--accent); 932 | } 933 | 934 | #branding h1, #branding h1 a:link, #branding h1 a:visited { 935 | color: #fff; 936 | } 937 | 938 | #branding h2 { 939 | padding: 0 10px; 940 | font-size: 14px; 941 | margin: -8px 0 8px 0; 942 | font-weight: normal; 943 | color: var(--header-color); 944 | } 945 | 946 | #branding a:hover { 947 | text-decoration: none; 948 | } 949 | 950 | #user-tools { 951 | float: right; 952 | padding: 0; 953 | margin: 0 0 0 20px; 954 | font-weight: 300; 955 | font-size: 11px; 956 | letter-spacing: 0.5px; 957 | text-transform: uppercase; 958 | text-align: right; 959 | } 960 | 961 | #user-tools a { 962 | border-bottom: 1px solid rgba(255, 255, 255, 0.25); 963 | } 964 | 965 | #user-tools a:focus, #user-tools a:hover { 966 | text-decoration: none; 967 | border-bottom-color: var(--primary); 968 | color: var(--primary); 969 | } 970 | 971 | /* SIDEBAR */ 972 | 973 | #content-related { 974 | background: var(--darkened-bg); 975 | } 976 | 977 | #content-related .module { 978 | background: none; 979 | } 980 | 981 | #content-related h3 { 982 | color: var(--body-quiet-color); 983 | padding: 0 16px; 984 | margin: 0 0 16px; 985 | } 986 | 987 | #content-related h4 { 988 | font-size: 13px; 989 | } 990 | 991 | #content-related p { 992 | padding-left: 16px; 993 | padding-right: 16px; 994 | } 995 | 996 | #content-related .actionlist { 997 | padding: 0; 998 | margin: 16px; 999 | } 1000 | 1001 | #content-related .actionlist li { 1002 | line-height: 1.2; 1003 | margin-bottom: 10px; 1004 | padding-left: 18px; 1005 | } 1006 | 1007 | #content-related .module h2 { 1008 | background: none; 1009 | padding: 16px; 1010 | margin-bottom: 16px; 1011 | border-bottom: 1px solid var(--hairline-color); 1012 | font-size: 18px; 1013 | color: var(--body-fg); 1014 | } 1015 | 1016 | .delete-confirmation form input[type="submit"] { 1017 | background: var(--delete-button-bg); 1018 | border-radius: 4px; 1019 | padding: 10px 15px; 1020 | color: var(--button-fg); 1021 | } 1022 | 1023 | .delete-confirmation form input[type="submit"]:active, 1024 | .delete-confirmation form input[type="submit"]:focus, 1025 | .delete-confirmation form input[type="submit"]:hover { 1026 | background: var(--delete-button-hover-bg); 1027 | } 1028 | 1029 | .delete-confirmation form .cancel-link { 1030 | display: inline-block; 1031 | vertical-align: middle; 1032 | height: 15px; 1033 | line-height: 15px; 1034 | border-radius: 4px; 1035 | padding: 10px 15px; 1036 | color: var(--button-fg); 1037 | background: var(--close-button-bg); 1038 | margin: 0 0 0 10px; 1039 | } 1040 | 1041 | .delete-confirmation form .cancel-link:active, 1042 | .delete-confirmation form .cancel-link:focus, 1043 | .delete-confirmation form .cancel-link:hover { 1044 | background: var(--close-button-hover-bg); 1045 | } 1046 | 1047 | /* POPUP */ 1048 | .popup #content { 1049 | padding: 20px; 1050 | } 1051 | 1052 | .popup #container { 1053 | min-width: 0; 1054 | } 1055 | 1056 | .popup #header { 1057 | padding: 10px 20px; 1058 | } 1059 | --------------------------------------------------------------------------------