diff --git a/.gitignore b/.gitignore index 7be4bd1b..5351fbde 100644 --- a/.gitignore +++ b/.gitignore @@ -25,6 +25,8 @@ share/python-wheels/ .installed.cfg *.egg MANIFEST +yarn.lock +package-lock.json # PyInstaller # Usually these files are written by a python script from a template @@ -61,7 +63,9 @@ local_settings.py db.sqlite3 db.sqlite3-journal *.dump +src/static/node_modules src/staticfiles +src/staticroot src/media # Flask stuff: diff --git a/.vscode/settings.json b/.vscode/settings.json index 6a275c6d..4e2b76c5 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -14,5 +14,6 @@ "python.testing.pytestEnabled": true, "python.testing.pytestArgs": ["src"], // linter - "ruff.organizeImports": true + "ruff.organizeImports": true, + "git.ignoreLimitWarning": true } diff --git a/pyproject.toml b/pyproject.toml index 4b334868..90b3acde 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,7 +18,7 @@ dependencies = [ "django-celery-beat>=2.6.0", "django-celery-results>=2.5.1", "django-crispy-forms>=2.1", - "django-handyhelpers>=0.3.22", + "django-handyhelpers>=0.3.28", "django-markdownify>=0.9.3", "django-storages[azure]>=1.14.2", "dj-database-url>=2.1.0", diff --git a/requirements.dev.lock b/requirements.dev.lock index c6e1b78c..71cc6440 100644 --- a/requirements.dev.lock +++ b/requirements.dev.lock @@ -12,6 +12,10 @@ asgiref==3.8.1 # via # spokanetech (pyproject.toml) # django +async-timeout==4.0.3 + # via + # aiohttp + # redis attrs==24.2.0 # via aiohttp azure-common==1.1.28 @@ -131,7 +135,7 @@ django-debug-toolbar==4.4.6 # via spokanetech (pyproject.toml) django-filter==24.3 # via djangorestframework-filters -django-handyhelpers==0.3.26 +django-handyhelpers==0.3.28 # via spokanetech (pyproject.toml) django-markdownify==0.9.5 # via spokanetech (pyproject.toml) @@ -149,6 +153,8 @@ drf-dynamic-fields==0.4.0 # via django-handyhelpers eventbrite==3.3.5 # via spokanetech (pyproject.toml) +exceptiongroup==1.2.2 + # via pytest flower==2.0.1 # via spokanetech (pyproject.toml) freezegun==1.5.1 @@ -349,10 +355,13 @@ tinycss2==1.2.1 # bleach # cairosvg # cssselect2 +tomli==2.0.1 + # via pytest tornado==6.4.1 # via flower typing-extensions==4.12.2 # via + # asgiref # azure-core # azure-storage-blob # beautifulsoup4 diff --git a/requirements.lock b/requirements.lock index bcd2afbd..9850aefe 100644 --- a/requirements.lock +++ b/requirements.lock @@ -12,6 +12,10 @@ asgiref==3.8.1 # via # spokanetech (pyproject.toml) # django +async-timeout==4.0.3 + # via + # aiohttp + # redis attrs==24.2.0 # via aiohttp azure-common==1.1.28 @@ -110,7 +114,7 @@ django-crispy-forms==2.3 # crispy-bootstrap5 django-filter==24.3 # via djangorestframework-filters -django-handyhelpers==0.3.26 +django-handyhelpers==0.3.28 # via spokanetech (pyproject.toml) django-markdownify==0.9.5 # via spokanetech (pyproject.toml) @@ -242,6 +246,7 @@ tornado==6.4.1 # via flower typing-extensions==4.12.2 # via + # asgiref # azure-core # azure-storage-blob # beautifulsoup4 diff --git a/src/spokanetech/settings.py b/src/spokanetech/settings.py index 29ab9b6a..64f05af2 100644 --- a/src/spokanetech/settings.py +++ b/src/spokanetech/settings.py @@ -39,7 +39,10 @@ # SECURITY WARNING: don't run with debug turned on in production! DEBUG = True - ALLOWED_HOSTS = [] + ALLOWED_HOSTS = [ + "localhost", + "127.0.0.1", + ] INTERNAL_IPS = [ "localhost", @@ -280,26 +283,6 @@ } -# Django Debug Toolbar -DEBUG_TOOLBAR_CONFIG = { - "DISABLE_PANELS": { - "debug_toolbar.panels.cache.CachePanel", - "debug_toolbar.panels.headers.HeadersPanel", - "debug_toolbar.panels.history.HistoryPanel", - "debug_toolbar.panels.profiling.ProfilingPanel", - "debug_toolbar.panels.redirects.RedirectsPanel", - "debug_toolbar.panels.request.RequestPanel", - "debug_toolbar.panels.settings.SettingsPanel", - "debug_toolbar.panels.signals.SignalsPanel", - "debug_toolbar.panels.sql.SQLPanel", - "debug_toolbar.panels.staticfiles.StaticFilesPanel", - "debug_toolbar.panels.templates.TemplatesPanel", - "debug_toolbar.panels.timer.TimerPanel", - "debug_toolbar.panels.versions.VersionsPanel", - } -} - - # Crispy Forms CRISPY_ALLOWED_TEMPLATE_PACKS = "bootstrap5" diff --git a/src/static/spokanetech/css/index-styles.css b/src/static/spokanetech/css/index-styles.css new file mode 100644 index 00000000..a2ba39fb --- /dev/null +++ b/src/static/spokanetech/css/index-styles.css @@ -0,0 +1,470 @@ +:root { + --light: #fff; + --dark: #111; + --gunmetal: hsla(197, 36%, 15%, 1); + --keppel: hsla(169, 59%, 45%, 1); + --persian-green: hsla(171, 58%, 40%, 1); + --pine-green: hsla(171, 46%, 31%, 1); + --rich-black: hsla(203, 32%, 8%, 1); + --accent-color: var(--keppel); +} +.text-primary { + color: var(--keppel) !important; +} +.text-secondary { + color: var(--light) !important; +} + +/* Reset some default styles */ +* { + margin: 0; + padding: 0; + box-sizing: border-box; + } + + a { + text-decoration: none; + color: var(--accent-color); +} + +body { + font-family: Arial, sans-serif; + background-image: url("../img/bg-02.jpg"); + background-size: cover; /* or 'contain' or specific size like '100px 100px' */ + background-repeat: no-repeat; /* or 'repeat' or 'repeat-x' or 'repeat-y' */ + background-position: center; /* or 'top', 'bottom', 'left', 'right', 'center' */ + background-attachment: fixed; /* or 'scroll' */ + display: flex; + flex-direction: column; + min-height: 100vh; + color: var(--light); +} + +main { + flex: 1; + text-align: center; + margin-top: 4rem; + margin-bottom: 2rem; + padding: 3rem; +} + +/* Navbar styles */ +.navbar { + display: flex; + justify-content: space-between; + align-items: center; + padding: 20px; + background-color: transparent; + color: var(--light); + position: fixed; + width: 100%; + top: 0; + left: 0; + z-index: 1000; + padding-left: 3rem; + padding-right: 3rem; +} + +.navbar .logo { + font-size: 1rem; + font-weight: bold; +} +.navbar .logo a { + color: var(--light); + opacity: .5; + text-decoration: none; + transition: opacity 0.3s ease; +} +.navbar .logo a:hover { + text-decoration: none; + opacity: 1; + color: var(--light); +} +.navbar .menu { + list-style: none; + display: flex; + gap: .25rem; +} +.navbar .menu li { + display: inline; +} +.navbar .menu a { + padding: 10px 15px; + color: var(--light); + opacity: .5; + text-decoration: none; + transition: opacity 0.3s ease; +} +.navbar .menu a:hover { + text-decoration: none; + opacity: 1; + color: var(--light); +} + +.hero { + display: flex; + justify-content: left; + align-items: center; + text-align: left; + color: var(--light); + margin-bottom: 2rem; +} + +.hero-content { + max-width: 90%; +} + +.hero-content .accent { + font-style: italic; +} + +.hero h1 { + font-size: 4.5rem; + font-weight: bold; + margin-bottom: 1rem; +} + +.hero h2 { + font-size: 2.5rem; + margin-bottom: 20px; +} + + +.hero p { + font-size: 1.2rem; +} + +footer { + color: var(--light); + padding: 10px 20px; + width: 100%; + bottom: 0; + text-align: center; +} + +footer p { + margin: 5px 0; +} + +footer a { + color: var(--light); + text-decoration: none; +} + +footer a:hover { + text-decoration: underline; +} + +footer .title { + font-size: 1.5rem; + font-weight: bold; + opacity: 50%; + margin-bottom: .5rem; +} + +footer .copyright { + font-size: .75rem; + opacity: 50%; +} + + +.hero-buttons { + margin-top: 2.5rem; + margin-bottom: 1rem; +} + +.hero-buttons .btn { + padding: 10px 20px; + font-size: 1rem; + border-radius: 5px; + border: 2px solid transparent; + cursor: pointer; + transition: all 0.3s ease; + margin-right: 10px; + margin-top: 10px; +} + +/* .hero-buttons .btn-filled { + background-color: #007bff; + color: var(--light); +} + +.hero-buttons .btn-filled:hover { + background-color: #0056b3; +} */ + +/* .hero-buttons .btn-outline { + background-color: transparent; + color: #007bff; + border-color: #007bff; +} + +.hero-buttons .btn-outline:hover { + background-color: #007bff; + color: var(--light); +} */ + +.hero-buttons .btn-outline { + background-color: transparent; + color: var(--accent-color); + border-color: var(--accent-color); +} + +.hero-buttons .btn-outline:hover { + background-color: var(--accent-color); + color: var(--light); + box-shadow: 0rem 0.25rem 1rem -0.25rem var(--accent-color); +} + +.empty-queryset { + color: var(--light); + margin-left: 5rem; + margin-right: 5rem; +} + + +.about h1 { + font-size: 2.5rem; + font-weight: bold; + margin-bottom: 2.5rem; +} + +.about p { + font-size: 1.5rem; + line-height: 1.5; + margin-bottom: 1.5rem; +} + +.marquee-wrapper { + margin-top: .5rem; + margin: 0rem !important; +} + +.marquee-wrapper a { + text-decoration: none; +} + +.marquee-menu { + text-align: start; + margin-top: .25rem; + margin-bottom: 1rem; +} +.marquee-menu ul { + list-style: none; + margin-right: 0rem; + padding-left: 0rem; +} +.marquee-menu li { + display: inline; + margin-right: 1rem; +} +.marquee-menu li a { + color: var(--light); + opacity: .5; + text-decoration: none; + transition: opacity 0.3s ease; +} +.marquee-menu li a:hover { + text-decoration: none; + opacity: 1; + color: var(--light); +} + +.card { + width: 250px; + height: 125px; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + background-color: rgba(255, 255, 255, 0.1); + border-radius: 0.5rem; + box-shadow: 0rem 0.25rem 1rem -0.25rem var(--light, rgb(255, 255, 255)); + text-align: center; + color: var(--light, rgb(255, 255, 255)); + font-weight: bold; + } + + .card:hover { + box-shadow: 0 4px 8px var(--accent, hsla(169, 59%, 45%, 1)); + color: var(--accent, hsla(169, 59%, 45%, 1)); + } + + .highlight { + color: var(--accent, hsla(169, 59%, 45%, 1)); + } + + + .list-card-container { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 3rem; + padding: 3rem; + } + + .list-card { + background-color: rgba(255, 255, 255, 0.2); + border-radius: 0.5rem; + box-shadow: 0rem 0.25rem 1rem -0.25rem var(--light, rgb(255, 255, 255)); + padding: 20px; + width: 400px; /* Fixed width */ + height: 150px; /* Fixed height */ + text-align: center; + display: flex; + flex-direction: column; + align-items: center; + justify-content: space-between; + color: var(--light, rgb(255, 255, 255)); + cursor: pointer; +} +.list-card .title { + font-weight: bold; + font-size: 2.5rem; +} +.list-card .description { + font-weight: bold; + font-size: 1.1rem; + margin-top: 1rem; + margin-bottom: 1rem; +} + +.tag-container { + margin:.75rem; +} + +.tag { + font-size: .7rem; + margin-left: .5rem; + margin-right: .5rem; + padding: .25rem .5rem; + color: var(--dark); + background-color: var(--accent-color); + border-radius: 999px; +} +.list-card .footer { + font-size: .75rem; + margin-top: .5rem; + color: var(--accent-color); + display: flex; + justify-content: space-between; +} +.list-card .footer a { + font-size: .75rem; + margin-top: .5rem; + color: var(--accent-color); + text-decoration: none !important; +} +.split-footer { + display: flex; + justify-content: space-between; /* Two columns, left and right justified */ + align-items: center; + width: 100%; + margin-top: .25rem; + font-size: .75rem; + /* color: var(--accent-color); */ + color: var(--light); + text-decoration: none !important; +} +.footer-left { + text-align: left; /* Left justify */ + flex: 1; /* Take up remaining space */ +} + +.footer-right { + text-align: right; /* Right justify */ + flex: 1; /* Take up remaining space */ +} + + +.list-card:hover { + box-shadow: 0 4px 8px var(--accent, hsla(169, 59%, 45%, 1)); + /* color: var(--accent, hsla(169, 59%, 45%, 1)); */ +} +.list-card:hover .title { + color: var(--accent, hsla(169, 59%, 45%, 1)); +} +.list-card:hover .description { + color: var(--accent, hsla(169, 59%, 45%, 1)); +} +.list-card:hover .split-footer { + color: var(--accent, hsla(169, 59%, 45%, 1)); +} + + +.detail h1 { + font-size: 2.5rem; +} +.homepage { + margin: .5rem; + font-size: .75rem; +} +.homepage a { + margin: .5rem; + color: var(--light); + opacity: .5; + text-decoration: none; + transition: opacity 0.3s ease; +} +.homepage a:hover { + text-decoration: none; + opacity: 1; + color: var(--light); +} +.description { + margin: 3rem; +} + + +/* Custom CSS for Bootstrap 5 Modals */ + +/* Override modal backdrop to make it semi-transparent */ +.modal-backdrop.show { + opacity: 0.5; /* Adjust the opacity as needed */ +} + +/* Override modal content to have a semi-transparent background */ +.modal-content { + background-color: hsla(197, 36%, 15%, .75); /* Adjust the opacity as needed */ + border: 2px solid var(--gunmetal); + backdrop-filter: blur(10px); +} + + +/* Apply accent color to modal buttons */ +.modal-footer .btn { + background-color: var(--accent-color); + border-color: var(--accent-color); + color: white; +} + +.modal-footer .btn:hover { + background-color: var(--accent-color); + opacity: 0.9; /* Optional: Add hover effect */ +} + +.modal-footer .btn-secondary { + background-color: #6c757d; /* Bootstrap secondary color */ + border-color: #6c757d; + color: white; +} + +.modal-footer .btn-secondary:hover { + background-color: #5a6268; /* Darken secondary color on hover */ +} +.modal-header, +.modal-footer { + border: none; /* Remove the default Bootstrap borders */ +} +.btn-close { + filter: invert(100%); +} + + +.calendar-container { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 3rem; + padding: 3rem; + } diff --git a/src/static/main.css b/src/static/spokanetech/css/main.css similarity index 92% rename from src/static/main.css rename to src/static/spokanetech/css/main.css index 314edb8e..4bba9c24 100644 --- a/src/static/main.css +++ b/src/static/spokanetech/css/main.css @@ -3,10 +3,6 @@ li { list-style-type: disc !important; } -.navbar li { - list-style-type: none !important; -} - .fit-content { width: fit-content; } diff --git a/src/static/android-chrome-192x192.png b/src/static/spokanetech/img/android-chrome-192x192.png similarity index 100% rename from src/static/android-chrome-192x192.png rename to src/static/spokanetech/img/android-chrome-192x192.png diff --git a/src/static/android-chrome-512x512.png b/src/static/spokanetech/img/android-chrome-512x512.png similarity index 100% rename from src/static/android-chrome-512x512.png rename to src/static/spokanetech/img/android-chrome-512x512.png diff --git a/src/static/apple-touch-icon.png b/src/static/spokanetech/img/apple-touch-icon.png similarity index 100% rename from src/static/apple-touch-icon.png rename to src/static/spokanetech/img/apple-touch-icon.png diff --git a/src/static/spokanetech/img/bg-01.jpg b/src/static/spokanetech/img/bg-01.jpg new file mode 100644 index 00000000..a6af15e4 Binary files /dev/null and b/src/static/spokanetech/img/bg-01.jpg differ diff --git a/src/static/spokanetech/img/bg-02.jpg b/src/static/spokanetech/img/bg-02.jpg new file mode 100644 index 00000000..950dbc66 Binary files /dev/null and b/src/static/spokanetech/img/bg-02.jpg differ diff --git a/src/static/favicon-16x16.png b/src/static/spokanetech/img/favicon-16x16.png similarity index 100% rename from src/static/favicon-16x16.png rename to src/static/spokanetech/img/favicon-16x16.png diff --git a/src/static/favicon-32x32.png b/src/static/spokanetech/img/favicon-32x32.png similarity index 100% rename from src/static/favicon-32x32.png rename to src/static/spokanetech/img/favicon-32x32.png diff --git a/src/static/favicon.ico b/src/static/spokanetech/img/favicon.ico similarity index 100% rename from src/static/favicon.ico rename to src/static/spokanetech/img/favicon.ico diff --git a/src/templates/spokanetech/base.html b/src/templates/spokanetech/base.html index a2e178f8..e290cb34 100644 --- a/src/templates/spokanetech/base.html +++ b/src/templates/spokanetech/base.html @@ -1,48 +1,96 @@ -{% extends "handyhelpers/handyhelpers_with_sidebar.htm" %} {% load static %} -{% block favicon %} - - - - - -Spokane Tech -{% endblock favicon %} - -{% block app_javascript %} -{% comment %} - -{% endcomment %} -{% endblock app_javascript %} - -{% block sidebar %} -{% include 'spokanetech/sidebar.htm' %} -{% endblock sidebar %} - -{% block mainnavbar %} -{% include 'spokanetech/navbar.htm' %} -{% endblock mainnavbar %} - -{% block mainfooter %} -{% include 'spokanetech/footer.htm' %} -{% endblock mainfooter%} + + + + + + + + + + Spokane Tech + + + + + + + + + + + + + + + + +
+ +
+ + {% block main_content %} +
+
+ {% endblock main_content %} + + + + {% block modals %} + {% if not disable_modals_in_base %} + {% include 'handyhelpers/htmx/bs5/generic_modal.htm' %} + {% endif %} + {% endblock modals %} + + + + + + + + + + + + + + + diff --git a/src/web/models.py b/src/web/models.py index 2cf49113..0f17be4b 100644 --- a/src/web/models.py +++ b/src/web/models.py @@ -41,7 +41,10 @@ def __str__(self) -> str: return self.name def get_absolute_url(self) -> str: - return reverse("web:get_tech_group", kwargs={"pk": self.pk}) + return reverse("web:get_techgroup", kwargs={"pk": self.pk}) + + # def get_htmx_detail_url(self) -> str: + # return reverse("web:get_techgroup", kwargs={"pk": self.pk}) class EventQuerySet(models.QuerySet): @@ -111,6 +114,9 @@ def __str__(self) -> str: def get_absolute_url(self) -> str: return reverse("web:get_event", kwargs={"pk": self.pk}) + # def get_htmx_detail_url(self) -> str: + # return reverse("web:detail_event", kwargs={"pk": self.pk}) + class EventbriteOrganization(models.Model): tech_group = models.ForeignKey(TechGroup, on_delete=models.CASCADE) diff --git a/src/web/scripts/__init__.py b/src/web/scripts/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/web/scripts/generate_dev_data.py b/src/web/scripts/generate_dev_data.py new file mode 100644 index 00000000..b2454bc9 --- /dev/null +++ b/src/web/scripts/generate_dev_data.py @@ -0,0 +1,65 @@ +from web.models import TechGroup + + +def create_groups(): + print("INFO: creating TechGroup entries") + data_list = [ + { + "name": "Business Brew", + "icon": """""", + }, + { + "name": "Greater Spokane Inc", + "icon": """""", + }, + { + "name": "Ignite Northwest", + "icon": """""", + }, + { + "name": "INCH360", + "icon": """""", + }, + { + "name": "SP3NW", + "icon": """""", + }, + { + "name": "Spokane DevOps Meetup", + "icon": """""", + }, + { + "name": "Spokane Go Users Group", + "icon": """""", + }, + { + "name": "Spokane .NET Users Group", + "icon": """""", + }, + { + "name": "spokaneOS", + "icon": """""", + }, + { + "name": "Spokane Python User Group", + "icon": """""", + }, + { + "name": "Spokane Rust User Group", + "icon": """""", + }, + { + "name": "Spokane Tech Community", + "icon": """""", + }, + { + "name": "Spokane UX", + "icon": """""", + }, + ] + for data in data_list: + TechGroup.objects.get_or_create(name=data["name"], defaults=data) + + +def run(): + create_groups() diff --git a/src/web/templates/web/event_detail.html b/src/web/templates/web/event_detail.html deleted file mode 100644 index 15c6813c..00000000 --- a/src/web/templates/web/event_detail.html +++ /dev/null @@ -1,7 +0,0 @@ -{% extends 'spokanetech/base.html' %} - -{% block content %} -
- {% include "web/partials/detail_event.htm" with object=object %} -
-{% endblock content %} diff --git a/src/web/templates/web/event_list.html b/src/web/templates/web/event_list.html deleted file mode 100644 index 1aad62b7..00000000 --- a/src/web/templates/web/event_list.html +++ /dev/null @@ -1,5 +0,0 @@ -{% extends 'spokanetech/base.html' %} - -{% block content %} -{% include "web/partials/event_list.htm" %} -{% endblock content %} diff --git a/src/web/templates/web/full/custom/about.html b/src/web/templates/web/full/custom/about.html new file mode 100644 index 00000000..42042611 --- /dev/null +++ b/src/web/templates/web/full/custom/about.html @@ -0,0 +1,6 @@ +{% extends 'spokanetech/base.html' %} + +{% block main_content %} +
+
+{% endblock main_content %} diff --git a/src/web/templates/web/full/custom/calendar.html b/src/web/templates/web/full/custom/calendar.html new file mode 100644 index 00000000..efb84b3f --- /dev/null +++ b/src/web/templates/web/full/custom/calendar.html @@ -0,0 +1,6 @@ +{% extends 'spokanetech/base.html' %} + +{% block main_content %} +
+
+{% endblock main_content %} diff --git a/src/web/templates/web/full/custom/index.html b/src/web/templates/web/full/custom/index.html new file mode 100644 index 00000000..b91ee5fa --- /dev/null +++ b/src/web/templates/web/full/custom/index.html @@ -0,0 +1,6 @@ +{% extends 'spokanetech/base.html' %} + +{% block main_content %} +
+
+{% endblock main_content %} diff --git a/src/web/templates/web/full/detail/event.html b/src/web/templates/web/full/detail/event.html new file mode 100644 index 00000000..fe63762d --- /dev/null +++ b/src/web/templates/web/full/detail/event.html @@ -0,0 +1,6 @@ +{% extends 'spokanetech/base.html' %} + +{% block main_content %} +
+
+{% endblock main_content %} diff --git a/src/web/templates/web/full/detail/group.html b/src/web/templates/web/full/detail/group.html new file mode 100644 index 00000000..3397a765 --- /dev/null +++ b/src/web/templates/web/full/detail/group.html @@ -0,0 +1,6 @@ +{% extends 'spokanetech/base.html' %} + +{% block main_content %} +
+
+{% endblock main_content %} diff --git a/src/web/templates/web/full/list/events.html b/src/web/templates/web/full/list/events.html new file mode 100644 index 00000000..ad4aa631 --- /dev/null +++ b/src/web/templates/web/full/list/events.html @@ -0,0 +1,9 @@ +{% extends 'spokanetech/base.html' %} + +{% block main_content %} +
+ {% include 'web/partials/list/events.htm' %} +
+{% endblock main_content %} + + \ No newline at end of file diff --git a/src/web/templates/web/full/list/groups.html b/src/web/templates/web/full/list/groups.html new file mode 100644 index 00000000..578b1471 --- /dev/null +++ b/src/web/templates/web/full/list/groups.html @@ -0,0 +1,9 @@ +{% extends 'spokanetech/base.html' %} + +{% block main_content %} +
+ {% include 'web/partials/list/groups.htm' %} +
+{% endblock main_content %} + + \ No newline at end of file diff --git a/src/web/templates/web/partials/custom/about.htm b/src/web/templates/web/partials/custom/about.htm new file mode 100644 index 00000000..84d61d7f --- /dev/null +++ b/src/web/templates/web/partials/custom/about.htm @@ -0,0 +1,15 @@ +
+

About SpokaneTech.org

+

+ Welcome to SpokaneTech.org, a vibrant community of technology enthusiasts based in Spokane and the greater Inland Northwest. Our mission is to foster community, knowledge sharing, and growth among individuals who are passionate about technology. +

+

+ Founded on the principles of collaboration and innovation, SpokaneTech.org brings together tech professionals, hobbyists, students, and anyone with an interest in technology. We believe that by working together and sharing knowledge, we can drive personal and professional growth, as well as contribute to the technological advancement of our community. +

+

+ At SpokaneTech.org, we host regular meetups, workshops, and events where members can connect, learn, and share their experiences. Our events cover a wide range of topics, from software development and cybersecurity to emerging technologies like AI and blockchain. Whether you're a seasoned professional or just starting your tech journey, there's always something for everyone. +

+

+ Join us at SpokaneTech.org and become a part of a supportive and dynamic community that is dedicated to making a positive impact through technology. Together, we can achieve great things and help shape the future of technology in Spokane and beyond. +

+
\ No newline at end of file diff --git a/src/web/templates/web/partials/custom/calendar.htm b/src/web/templates/web/partials/custom/calendar.htm new file mode 100644 index 00000000..ba0d64fb --- /dev/null +++ b/src/web/templates/web/partials/custom/calendar.htm @@ -0,0 +1,135 @@ + +
+

{{ title }}

+

{{ month_name }} {{ year }}

+
+ +
+ + +
+ + + + + + + + + + + + + + + {% for week in cal_data %} + + {% for day in week %} + + {% endfor %} + + {% endfor %} + +
MonTueWedThuFriSatSun
+ {% if day %} +
{{ day }}
+ {%endif%} + {% for event in event_list %} + {% if event.date_time.day == day and event.date_time.month == month and event.date_time.year == year %} + {% if event_detail_url %} +
{{ event }} +
+ {% else %} +
{{ event }}
+ {% endif %} + {% endif %} + {% endfor %} +
+
+
diff --git a/src/web/templates/web/partials/custom/index.htm b/src/web/templates/web/partials/custom/index.htm new file mode 100644 index 00000000..656f047b --- /dev/null +++ b/src/web/templates/web/partials/custom/index.htm @@ -0,0 +1,23 @@ +
+
+

Your Inland Northwest tech community

+
+ +
+
+
+
+
+ +
+
+
\ No newline at end of file diff --git a/src/web/templates/web/partials/detail/event.htm b/src/web/templates/web/partials/detail/event.htm new file mode 100644 index 00000000..d74c6b47 --- /dev/null +++ b/src/web/templates/web/partials/detail/event.htm @@ -0,0 +1,33 @@ +{% load markdownify %} +
+

{{ object.name }}

+
+ +
+
{{ object.date_time }}
+
+ {% if object.group %} + Hosted by:
+ {{ object.group }} + {% endif %} +
+
+ +
+
+ {% if object.description %} + {{ object.description|markdownify }} + {% else %} + No description + {% endif %} +
+
+ +
+
+ {% for tag in object.tags.all %} + {{ tag.value }} + {% endfor %} +
+
RSVP here
+
diff --git a/src/web/templates/web/partials/detail/group.htm b/src/web/templates/web/partials/detail/group.htm new file mode 100644 index 00000000..bcc7fd81 --- /dev/null +++ b/src/web/templates/web/partials/detail/group.htm @@ -0,0 +1,29 @@ +{% load markdownify %} +
+

{{ object.icon|safe }}

+

{{ object.name }}

+
+ +
+
+ {% if object.homepage %} + {{ object.homepage }} + {% endif %} +
+
+ +
+
+ {% for tag in object.tags.all %} + {{ tag.value }} + {% endfor %} +
+
+ {% if object.description %} + {{ object.description|markdownify }} + {% else %} + No description + {% endif %} +
+
+ \ No newline at end of file diff --git a/src/web/templates/web/partials/detail_event.htm b/src/web/templates/web/partials/detail_event.htm deleted file mode 100644 index 6e0ff77f..00000000 --- a/src/web/templates/web/partials/detail_event.htm +++ /dev/null @@ -1,64 +0,0 @@ -{% load web_extras markdownify %} -
-

- {{ object }} -

- -
- {% include 'spokanetech/partials/human_readable_datetime.htm' with object=object only %} -
- - {% if object.image %} -
- -
- {% endif %} - - {% if object.group %} -
- -
- {% endif %} - -

-

Description

- {{ object.description|markdownify }} -

- - {% with tags=object.tags.all %} - {% if tags %} -

-

Tags

- -

- {% elif object.group.tags.all %} -

-

Tags

- -

- {% endif %} - {% endwith %} - - {% if event.url %} - - RSVP - - {% endif %} - {% if can_edit %} - - Edit - - {% endif %} -
diff --git a/src/web/templates/web/partials/detail_tech_group.htm b/src/web/templates/web/partials/detail_tech_group.htm deleted file mode 100644 index c309a580..00000000 --- a/src/web/templates/web/partials/detail_tech_group.htm +++ /dev/null @@ -1,44 +0,0 @@ -{% load markdownify %} -
-

- - {{ object }} -

- {% if object.homepage %} - {{ object.homepage }} - {% endif %} -
- {% if object.description %} - {{ object.description|markdownify }} - {% else %} - No description - {% endif %} - - {% with tags=object.tags.all %} - {% if tags %} -

-

Tags

- -

- {% endif %} - {% endwith %} - - {% if can_edit %} - - Edit - - {% endif %} - - {% with upcoming_events=object.event_set.all %} - {% if upcoming_events %} -

Upcoming Events:

-
- {% include "web/partials/event_list.htm" with queryset=upcoming_events %} -
- {% endif %} - {% endwith %} -
diff --git a/src/web/templates/web/partials/event_list.htm b/src/web/templates/web/partials/event_list.htm deleted file mode 100644 index 002c5752..00000000 --- a/src/web/templates/web/partials/event_list.htm +++ /dev/null @@ -1,63 +0,0 @@ -{% extends 'web/partials/generic_list.htm' %} -{% load web_extras markdownify %} - -{% block content %} -
- {% for object in queryset %} -
- {% if object.image %} - - - - {% endif %} -
-
- - {{ object }} - -
- - {% include 'spokanetech/partials/human_readable_datetime.htm' with object=object duration=object.duration only %} - -
-
- {% if object.tags.all %} -
- {% for tag in object.tags.all %} - {{ tag }} - {% endfor %} -
- {% elif object.group.tags.all %} -
- {% for tag in object.group.tags.all %} - {{ tag }} - {% endfor %} -
- {% endif %} - - {{ object.description|markdownify|truncatewords_html:50 }} -
-
- {% if object.group %} - - {{ object.group }} - - {% endif %} - {% if object.url %} - - {% endif %} -
-
- {% endfor %} -
- -
- - {% if request.user.is_staff %}Add Event{% else %}Suggest Event{% endif %} - -
-{% endblock content %} diff --git a/src/web/templates/web/partials/generic_list.htm b/src/web/templates/web/partials/generic_list.htm deleted file mode 100644 index 8da09088..00000000 --- a/src/web/templates/web/partials/generic_list.htm +++ /dev/null @@ -1,79 +0,0 @@ -{% load handyhelper_tags web_extras markdownify %} -{# much of this code is from https://github.com/davidslusser/django-handyhelpers/blob/main/handyhelpers/templates/handyhelpers/generic/bs5/generic_list_content.htm #} - -
-
-

{{ title }}

-
-
- {# include extra_controls #} - {% if extra_controls.items %} - {% for name, control in extra_controls.items %} - {{ control.icon|safe }} - {% endfor %} - {% endif %} - - {# To include a create form, exposed via modal, include create_form (dict) in the context of your view. The create_from must include modal_name and link_title fields #} - {% if create_form %} - {% if allow_create_groups and request.user|in_any_group:allow_create_groups %} - - - {% if create_form.link_title %} {{ create_form.link_title }} {% endif %} - - {% endif %} - {% endif %} - - {# To include a filter form, exposed via modal, include filter_form (dict) in the context of your view. The filter_from must include modal_name and link_title fields #} - {% if filter_form %} - - - {% if filter_form.link_title %} {{ filter_form.link_title }} {% endif %} - - {% if filter_form.undo and request.META.QUERY_STRING %} - - - - {% endif %} - {% endif %} -
-
- -{% block content %}{% endblock content %} - -{% if is_paginated_view %} -{% include 'handyhelpers/generic/bs5/partials/pagination_controls.htm' %} -{% endif %} - -{# include classic modals if create or filter form is included #} -{% if create_form or filter_form %} - {% include 'handyhelpers/component/bs5/modals.htm' %} -{% endif %} - -{# include generic modal form for the create object form if passed from the view #} -{% with create_form as form_data %} - {% include 'handyhelpers/generic/bs5/generic_modal_form.htm' %} -{% endwith %} - -{# include generic modal form for the filter object form if passed from the view #} -{% with filter_form as form_data %} - {% include 'handyhelpers/generic/bs5/generic_modal_form.htm' %} -{% endwith %} - -{# include custom modal html/js template if passed in from the view #} -{% if modals %} - {% include modals %} -{% endif %} - -{# block for additional static content #} -{% block additional_static %} -{% if add_static %} -{{ add_static|safe }} -{% endif %} -{% endblock additional_static %} - -{# block for additional template content #} -{% block additional_template %} -{% if add_template %} -{% include add_template %} -{% endif %} -{% endblock additional_template %} diff --git a/src/web/templates/web/partials/list/events.htm b/src/web/templates/web/partials/list/events.htm new file mode 100644 index 00000000..8668c60f --- /dev/null +++ b/src/web/templates/web/partials/list/events.htm @@ -0,0 +1,20 @@ +
+

Upcoming Events

+ +
+ {% for row in queryset %} +
+
{{ row.name }}
+
+ {% for tag in row.tags.all %} + {{ tag }} + {% endfor %} +
+ +
+ {% endfor %} +
+
diff --git a/src/web/templates/web/partials/list/groups.htm b/src/web/templates/web/partials/list/groups.htm new file mode 100644 index 00000000..8422fbd0 --- /dev/null +++ b/src/web/templates/web/partials/list/groups.htm @@ -0,0 +1,17 @@ +
+

Our Tech Groups

+ +
+ {% for row in queryset %} +
+
{{ row.icon|safe }}
+
{{ row.name }}
+ +
+ {% endfor %} +
+
diff --git a/src/web/templates/web/partials/marquee/event_cards.htm b/src/web/templates/web/partials/marquee/event_cards.htm new file mode 100644 index 00000000..28202290 --- /dev/null +++ b/src/web/templates/web/partials/marquee/event_cards.htm @@ -0,0 +1,15 @@ +{% if queryset %} +{% for row in queryset %} +
{{ row.name }}
+{% endfor %} +{% else %} +
+ No events currently scheduled +
+{% endif %} + diff --git a/src/web/templates/web/partials/marquee/group_cards.htm b/src/web/templates/web/partials/marquee/group_cards.htm new file mode 100644 index 00000000..843e02cc --- /dev/null +++ b/src/web/templates/web/partials/marquee/group_cards.htm @@ -0,0 +1,9 @@ +{% for row in queryset %} +
{{ row.name }}
+ +{% endfor %} diff --git a/src/web/templates/web/partials/modal/detail_event.htm b/src/web/templates/web/partials/modal/event_information.htm similarity index 70% rename from src/web/templates/web/partials/modal/detail_event.htm rename to src/web/templates/web/partials/modal/event_information.htm index 14c2f355..df99ee5b 100644 --- a/src/web/templates/web/partials/modal/detail_event.htm +++ b/src/web/templates/web/partials/modal/event_information.htm @@ -9,12 +9,12 @@ {% if object.description %}
Description:
-
{{ object.description|markdownify }}
+
{{ object.description|markdownify|truncatewords_html:30 }}
{% endif %} {% if object.date_time %}
-
When:
+
Date/Time:
{% include 'spokanetech/partials/human_readable_datetime.htm' with object=object duration=object.duration only %}
@@ -26,4 +26,10 @@
{{ object.location }}
{% endif %} +
+
+
+ full details +
+
diff --git a/src/web/templates/web/partials/modal/group_information.htm b/src/web/templates/web/partials/modal/group_information.htm new file mode 100644 index 00000000..184083a8 --- /dev/null +++ b/src/web/templates/web/partials/modal/group_information.htm @@ -0,0 +1,35 @@ +{% load web_extras markdownify %} +
+ {% if object.group %} +
+
Group:
+
{{ object.group.name }}
+
+ {% endif %} + {% if object.description %} +
+
Description:
+
{{ object.description|markdownify|truncatewords_html:30 }}
+
+ {% endif %} + {% if object.date_time %} +
+
Date/Time:
+
+ {% include 'spokanetech/partials/human_readable_datetime.htm' with object=object duration=object.duration only %} +
+
+ {% endif %} + {% if object.location %} +
+
Location:
+
{{ object.location }}
+
+ {% endif %} +
+
+ +
+
diff --git a/src/web/templates/web/partials/sidebar_items.htm b/src/web/templates/web/partials/sidebar_items.htm deleted file mode 100644 index ecba0fb8..00000000 --- a/src/web/templates/web/partials/sidebar_items.htm +++ /dev/null @@ -1,5 +0,0 @@ -{% for row in queryset %} - -{% endfor %} diff --git a/src/web/templates/web/partials/table/table_tech_groups.htm b/src/web/templates/web/partials/table/table_tech_groups.htm deleted file mode 100644 index fc58991e..00000000 --- a/src/web/templates/web/partials/table/table_tech_groups.htm +++ /dev/null @@ -1,29 +0,0 @@ -{% load markdownify %} - - - - - - - {% for object in queryset %} - - - - - {% endfor %} - -
NameHomepage
- - {% if object.icon %}{% endif %}{{ object.name }} - - - {% if object.homepage %} - {{ object.homepage }} - {% endif %} -
- -{% if kwargs.can_edit %} -
- Add -
-{% endif %} diff --git a/src/web/templates/web/partials/techgroup_list.htm b/src/web/templates/web/partials/techgroup_list.htm deleted file mode 100644 index c20e9522..00000000 --- a/src/web/templates/web/partials/techgroup_list.htm +++ /dev/null @@ -1,33 +0,0 @@ -{% load web_extras markdownify %} -

Tech Groups

- -
- {% for object in queryset %} -
- -
- {% if object.tags.all %} -
- {% for tag in object.tags.all %} - {{ tag }} - {% endfor %} -
- {% endif %} - {{ object.description|markdownify|truncatewords_html:50 }} -
- -
- {% endfor %} -
diff --git a/src/web/templates/web/techgroup_detail.html b/src/web/templates/web/techgroup_detail.html deleted file mode 100644 index 6292effb..00000000 --- a/src/web/templates/web/techgroup_detail.html +++ /dev/null @@ -1,5 +0,0 @@ -{% extends 'spokanetech/base.html' %} - -{% block content %} -{% include "web/partials/detail_tech_group.htm" %} -{% endblock content %} diff --git a/src/web/templates/web/techgroup_form.html b/src/web/templates/web/techgroup_form.html deleted file mode 100644 index ec25512f..00000000 --- a/src/web/templates/web/techgroup_form.html +++ /dev/null @@ -1,7 +0,0 @@ -{% extends 'spokanetech/base.html' %} -{% load crispy_forms_tags %} - -{% block content %} -{% if object %}

Edit Tech Group

{% else %}

Add Tech Group

{% endif %} -{% crispy form form.helper %} -{% endblock content %} diff --git a/src/web/templates/web/techgroup_list.html b/src/web/templates/web/techgroup_list.html deleted file mode 100644 index 8233e384..00000000 --- a/src/web/templates/web/techgroup_list.html +++ /dev/null @@ -1,5 +0,0 @@ -{% extends 'spokanetech/base.html' %} - -{% block content %} -{% include "web/partials/techgroup_list.htm" %} -{% endblock content %} diff --git a/src/web/tests/test_views.py b/src/web/tests/test_views.py index 9ee9f50e..6e9e5688 100644 --- a/src/web/tests/test_views.py +++ b/src/web/tests/test_views.py @@ -2,98 +2,13 @@ import zoneinfo from typing import Any -import freezegun -import pytest -from bs4 import BeautifulSoup from django.contrib.auth import get_user_model from django.test import TestCase -from django.test.client import Client from django.urls import reverse from django.utils import timezone from model_bakery import baker -from web.models import Event, TechGroup - - -@pytest.mark.django_db -def test_list_tech_groups(client: Client): - # Arrange - tech_group = TechGroup( - name="List Tech Groups Test", - description="List Tech Groups Test", - enabled=True, - homepage="https://spokanetech.org/", - ) - tech_group.save() - - # Act - url = reverse("web:list_tech_groups") - response = client.get(url) - - # Assert - assert response.status_code == 200 - assert response.context["queryset"].get().pk == tech_group.pk - - -@pytest.mark.django_db -def test_get_tech_group(client: Client): - # Arrange - tech_group = TechGroup( - name="Get Tech Groups Test", - description="Get Tech Groups Test", - enabled=True, - homepage="https://spokanetech.org/", - ) - tech_group.save() - - # Act - url = reverse("web:get_tech_group", args=[tech_group.pk]) - response = client.get(url) - - # Assert - assert response.status_code == 200 - assert response.context["object"].pk == tech_group.pk - - -@freezegun.freeze_time("2024-03-17") -@pytest.mark.django_db -def test_set_timezone_and_timezone_middleware(client: Client): - # Arrange - date_time = datetime.datetime.fromisoformat("2024-03-19T01:00:00Z") - baker.make("web.Event", date_time=date_time, approved_at=date_time) - - # Act - client.post(reverse("web:set_timezone"), {"timezone": "America/Los_Angeles"}) - response = client.get(reverse("web:list_events")) - - # Assert - soup = BeautifulSoup(response.content, "lxml") - date_time_tag = soup.find(attrs={"data-testid": "date_time"}) - assert date_time_tag is not None - actual = date_time_tag.text.strip() - assert actual == "Monday, March 18, 2024 at 6:00 PM" - - -class TestEventDetailModal(TestCase): - """test GetEventDetailsModal view""" - - def setUp(self): - super(TestEventDetailModal, self).setUp() - self.object = baker.make("web.Event", approved_at=timezone.localtime()) - self.headers: dict[str, Any] = dict(HTTP_HX_REQUEST="true") - self.referrer = reverse("web:index") - self.url = reverse("web:get_event_details", kwargs={"pk": self.object.pk}) - - def test_get(self): - """verify modal content can be rendered""" - response = self.client.get(self.url, HTTP_REFERER=self.referrer, **self.headers) - self.assertEqual(response.status_code, 200) - self.assertTemplateUsed(response, "web/partials/modal/detail_event.htm") - - def test_non_htmx_call(self): - """verify 400 response if non-htmx request is used""" - response = self.client.get(self.url, HTTP_REFERER=self.referrer) - self.assertEqual(response.status_code, 400) +from web.models import Event class TestCreateEvent(TestCase): @@ -116,7 +31,7 @@ def test_suggest_event_redirects_and_sets_unapproved(self): # Assert assert response.status_code == 302 - assert response.url == reverse("web:list_events") + assert response.url == reverse("web:get_events") actual = Event.all.get() assert actual.approved_at is None @@ -134,9 +49,7 @@ def test_update_event_sets_right_date_time(self): user.is_staff = True # type: ignore user.save() - # set user TZ - self.client.post(reverse("web:set_timezone"), {"timezone": timezone_str}) - response = self.client.get(reverse("web:list_events")) + response = self.client.get(reverse("web:get_events")) assert response.status_code == 200 # Act @@ -191,51 +104,196 @@ def test_update_event_remove_approved_at_redirects_to_list(self): # Assert assert response.status_code == 302 - assert response.url == reverse("web:list_events") + assert response.url == reverse("web:get_events") + +class TestIndexView(TestCase): + def setUp(self): + super(TestIndexView, self).setUp() + self.headers: dict[str, Any] = dict(HTTP_HX_REQUEST="true") + self.now = timezone.now() + self.url = reverse("web:index") + + def test_default(self): + """verify call to GetIndexContent view via 'default' url with a non-htmx call""" + response = self.client.get(self.url) + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, "web/full/custom/index.html") + + def test_default_htmx(self): + """verify call to GetIndexContent view via 'default' with a htmx call""" + response = self.client.get(self.url, **self.headers) + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, "web/partials/custom/index.htm") + + +class TestAboutView(TestCase): + def setUp(self): + super(TestAboutView, self).setUp() + self.headers: dict[str, Any] = dict(HTTP_HX_REQUEST="true") + self.now = timezone.now() + self.url = reverse("web:about") + + def test_get(self): + """verify call to GetAboutContent view with a non-htmx call""" + response = self.client.get(self.url) + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, "web/full/custom/about.html") + + def test_get_htmx(self): + """verify call to GetAboutContent view with a htmx call""" + response = self.client.get(self.url, **self.headers) + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, "web/partials/custom/about.htm") -class TestEventCalendarView(TestCase): - """test EventCalendarView view""" +class TestCalendarView(TestCase): def setUp(self): - super(TestEventCalendarView, self).setUp() - self.object = baker.make("web.Event", approved_at=timezone.localtime()) + super(TestCalendarView, self).setUp() self.headers: dict[str, Any] = dict(HTTP_HX_REQUEST="true") - self.referrer = reverse("web:index") self.now = timezone.now() self.url = reverse("web:event_calendar", kwargs={"year": self.now.year, "month": self.now.month}) def test_get(self): - """verify page content can be rendered""" - response = self.client.get(self.url, HTTP_REFERER=self.referrer, **self.headers) + """verify call to EventCalendarView view with a non-htmx call""" + response = self.client.get(self.url) self.assertEqual(response.status_code, 200) - self.assertTemplateUsed(response, "handyhelpers/partials/calendar.htm") - self.assertIn(self.object.name, response.content.decode("utf-8")) - self.assertIn(f"/events/{self.object.pk}/details", response.content.decode("utf-8")) + self.assertTemplateUsed(response, "web/full/custom/calendar.html") + + def test_get_htmx(self): + """verify call to EventCalendarView view with a htmx call""" + response = self.client.get(self.url, **self.headers) + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, "web/partials/custom/calendar.htm") -class TestEventListView(TestCase): +class TestGetTechEventView(TestCase): def setUp(self): - super().setUp() - now = timezone.localtime() - self.tag = baker.make("web.Tag") - self.group = baker.make("web.TechGroup") - self.group.tags.set([self.tag]) - self.object = baker.make( - "web.Event", - group=self.group, - date_time=now + datetime.timedelta(seconds=1), - approved_at=now, - ) - self.url = reverse("web:list_events") + super(TestGetTechEventView, self).setUp() + self.instance = baker.make("web.Event", approved_at=timezone.localtime()) + self.headers: dict[str, Any] = dict(HTTP_HX_REQUEST="true") + self.url = reverse("web:get_event", kwargs={"pk": self.instance.pk}) + + def test_get(self): + """verify call to GetTechEvent view with a non-htmx call""" + response = self.client.get(self.url) + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, "web/full/detail/event.html") + + def test_get_htmx(self): + """verify call to GetTechEvent view with a htmx call""" + response = self.client.get(self.url, **self.headers) + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, "web/partials/detail/event.htm") + self.assertIn(self.instance.name, response.content.decode("utf-8")) + + +class TestGetTechEventsView(TestCase): + def setUp(self): + super(TestGetTechEventsView, self).setUp() + self.instance = baker.make("web.Event", approved_at=timezone.localtime()) + self.headers: dict[str, Any] = dict(HTTP_HX_REQUEST="true") + + def test_get(self): + """verify call to GetTechEvents view with a non-htmx call""" + url = reverse("web:get_events") + response = self.client.get(url) + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, "web/full/list/events.html") + + def test_get_htmx_index(self): + """verify call to GetTechEvents view with a htmx call""" + url = reverse("web:get_events", kwargs={"display": "index"}) + response = self.client.get(url, **self.headers) + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, "web/partials/marquee/event_cards.htm") + self.assertIn(self.instance.name, response.content.decode("utf-8")) + + def test_get_htmx_list(self): + """verify call to GetTechEvents view with a htmx call""" + url = reverse("web:get_events", kwargs={"display": "list"}) + response = self.client.get(url, **self.headers) + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, "web/partials/list/events.htm") + self.assertIn(self.instance.name, response.content.decode("utf-8")) + + +class GetTechEventModalView(TestCase): + def setUp(self): + super(GetTechEventModalView, self).setUp() + self.instance = baker.make("web.Event", approved_at=timezone.localtime()) + self.headers: dict[str, Any] = dict(HTTP_HX_REQUEST="true") + self.url = reverse("web:techevent_modal", kwargs={"pk": self.instance.pk}) + + def test_get_htmx(self): + """verify call to GetTechEventModal view with a htmx call""" + response = self.client.get(self.url, **self.headers) + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, "web/partials/modal/event_information.htm") + self.assertIn(self.instance.name, response.content.decode("utf-8")) + - def test_filter_on_group_tags(self): - response = self.client.get(self.url + f"?tags={self.tag.pk}") +class TestGetTechGroupView(TestCase): + def setUp(self): + super(TestGetTechGroupView, self).setUp() + self.instance = baker.make("web.TechGroup") + self.headers: dict[str, Any] = dict(HTTP_HX_REQUEST="true") + self.url = reverse("web:get_techgroup", kwargs={"pk": self.instance.pk}) + + def test_get(self): + """verify call to GetTechGroup view with a non-htmx call""" + response = self.client.get(self.url) + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, "web/full/detail/group.html") + + def test_get_htmx(self): + """verify call to GetTechGroup view with a htmx call""" + response = self.client.get(self.url, **self.headers) + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, "web/partials/detail/group.htm") + self.assertIn(self.instance.name, response.content.decode("utf-8")) + + +class TestGetTechGroupsView(TestCase): + def setUp(self): + super(TestGetTechGroupsView, self).setUp() + self.instance = baker.make("web.TechGroup") + self.headers: dict[str, Any] = dict(HTTP_HX_REQUEST="true") + + def test_get(self): + """verify call to GetTechGroups view with a non-htmx call""" + url = reverse("web:get_techgroups") + response = self.client.get(url) + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, "web/full/list/groups.html") + + def test_get_htmx_index(self): + """verify call to GetTechGroups view with a htmx call""" + url = reverse("web:get_techgroups", kwargs={"display": "index"}) + response = self.client.get(url, **self.headers) + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, "web/partials/marquee/group_cards.htm") + self.assertIn(self.instance.name, response.content.decode("utf-8")) + + def test_get_htmx_list(self): + """verify call to GetTechGroups view with a htmx call""" + url = reverse("web:get_techgroups", kwargs={"display": "list"}) + response = self.client.get(url, **self.headers) self.assertEqual(response.status_code, 200) - self.assertIn(self.object.name, response.content.decode("utf-8")) + self.assertTemplateUsed(response, "web/partials/list/groups.htm") + self.assertIn(self.instance.name, response.content.decode("utf-8")) - def test_does_not_include_unapproved_events(self): - self.object.approved_at = None - self.object.save() - response = self.client.get(self.url + f"?tags={self.tag.pk}") - self.assertNotIn(self.object.name, response.content.decode("utf-8")) + +class GetTechGroupModalView(TestCase): + def setUp(self): + super(GetTechGroupModalView, self).setUp() + self.instance = baker.make("web.TechGroup") + self.headers: dict[str, Any] = dict(HTTP_HX_REQUEST="true") + self.url = reverse("web:techgroup_modal", kwargs={"pk": self.instance.pk}) + + def test_get_htmx(self): + """verify call to GetTechGroupModal view with a htmx call""" + response = self.client.get(self.url, **self.headers) + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, "web/partials/modal/group_information.htm") + self.assertIn(self.instance.name, response.content.decode("utf-8")) diff --git a/src/web/urls.py b/src/web/urls.py index e35c2093..512cde86 100644 --- a/src/web/urls.py +++ b/src/web/urls.py @@ -1,23 +1,21 @@ -from django.urls import path +from django.urls import path, re_path from web import views app_name = "web" urlpatterns = [ - path("", views.Index.as_view(), name="index"), - path("set_timezone/", views.set_timezone, name="set_timezone"), - path("events", views.ListEvents.as_view(), name="list_events"), - path("groups", views.ListTechGroup.as_view(), name="list_tech_groups"), - path("groups/add", views.CreateTechGroup.as_view(), name="add_tech_group"), - path("groups/", views.DetailTechGroup.as_view(), name="get_tech_group"), - path("groups//edit", views.UpdateTechGroup.as_view(), name="edit_tech_group"), + path("", views.GetIndexContent.as_view(), name="index"), + path("index", views.GetIndexContent.as_view(), name="index"), + path("about", views.GetAboutContent.as_view(), name="about"), + path("calendar//", views.EventCalendarView.as_view(), name="event_calendar"), path("events/add", views.CreateEvent.as_view(), name="add_event"), - path("events/", views.DetailEvent.as_view(), name="get_event"), + path("events/", views.GetTechEvent.as_view(), name="get_event"), + path("events//details", views.GetTechEventModal.as_view(), name="techevent_modal"), path("events//edit", views.UpdateEvent.as_view(), name="update_event"), - path("build_sidebar", views.BuildSidebar.as_view(), name="build_sidebar"), - path("events//details/", views.GetEventDetailsModal.as_view(), name="get_event_details"), - path("event_calendar///", views.EventCalendarView.as_view(), name="event_calendar"), - # handyhelpers overrides - path("filter_list_view", views.FilterListView.as_view(), name="filter_list_view"), + re_path("^events/(?P\w+)?$", views.GetTechEvents.as_view(), name="get_events"), + path("techgroups/add", views.CreateTechGroup.as_view(), name="add_tech_group"), + path("techgroups/", views.GetTechGroup.as_view(), name="get_techgroup"), + path("techgroups//details", views.GetTechGroupModal.as_view(), name="techgroup_modal"), + re_path("^techgroups/(?P\w+)?$", views.GetTechGroups.as_view(), name="get_techgroups"), ] diff --git a/src/web/views.py b/src/web/views.py index fab555fe..efd1ab0f 100644 --- a/src/web/views.py +++ b/src/web/views.py @@ -2,52 +2,22 @@ from django.contrib import messages from django.contrib.auth.mixins import UserPassesTestMixin -from django.db.models import Prefetch -from django.http import HttpRequest, HttpResponse -from django.shortcuts import redirect -from django.template import loader -from django.urls import reverse, reverse_lazy +from django.http import HttpRequest +from django.urls import reverse from django.utils import timezone -from django.views import View -from django.views.decorators.http import require_http_methods -from django.views.generic import CreateView, DetailView, UpdateView -from handyhelpers.mixins.view_mixins import HtmxViewMixin -from handyhelpers.views.calendar import CalendarView -from handyhelpers.views.gui import ( - HandyHelperIndexView, - HandyHelperListPlusFilterView, - HandyHelperListView, +from django.views.generic import CreateView, UpdateView +from handyhelpers.views.calendar import HtmxCalendarView +from handyhelpers.views.htmx import ( + ModelDetailBootstrapModalView, + HtmxOptionView, + HtmxOptionDetailView, + HtmxOptionMultiFilterView, ) -from handyhelpers.views.htmx import BuildBootstrapModalView, BuildModelSidebarNav from web import forms from web.models import Event, TechGroup -@require_http_methods(["POST"]) -def set_timezone(request: HttpRequest) -> HttpResponse: - timezone_id = request.POST["timezone"] - request.session["timezone"] = timezone_id - return HttpResponse() - - -class Index(HandyHelperIndexView): - title = "Spokane Tech" - subtitle = "Index of Spokane's Tech User Groups" - base_template = "spokanetech/base.html" - - def __init__(self, **kwargs: Any) -> None: - self.item_list = [ - { - "url": tech_group.get_absolute_url(), - "icon": tech_group.icon, - "title": str(tech_group), - } - for tech_group in TechGroup.objects.all() - ] - super().__init__(**kwargs) - - class CanEditMixin: """Add can_edit to the template context.""" @@ -69,62 +39,6 @@ def test_func(self) -> bool | None: return user.is_authenticated and user.is_staff # type: ignore -class ListEvents(CanEditMixin, HtmxViewMixin, HandyHelperListPlusFilterView): - title = "Events" - base_template = "spokanetech/base.html" - template_name = "web/event_list.html" - - filter_form_obj = forms.ListEventsFilter - filter_form_url = reverse_lazy("web:filter_list_view") - - def __init__(self, **kwargs: Any) -> None: - self.queryset = ( - Event.objects.filter(date_time__gte=timezone.localtime()) - .select_related("group") - .prefetch_related("tags", "group__tags") - ) - super().__init__(**kwargs) - - def get(self, request, *args, **kwargs): - if self.is_htmx(): - self.template_name = "web/partials/event_list.htm" - return super().get(request, *args, **kwargs) - - def filter_by_query_params(self): - """Include events that don't have any tags of their own but their group tags match.""" - queryset = super().filter_by_query_params() - if queryset is None: - return None - - if tags := self.request.GET.getlist("tags"): - events_with_group_tags_queryset = Event.objects.filter( - date_time__gte=timezone.localtime() - ).filter_group_tags(tags) # type: ignore - queryset = queryset | events_with_group_tags_queryset - - queryset = queryset.order_by("date_time") - return queryset - - -class DetailEvent(HtmxViewMixin, DetailView): - model = Event - - def __init__(self, **kwargs: Any) -> None: - self.queryset = Event.objects.select_related("group").prefetch_related("tags", "group__tags") - super().__init__(**kwargs) - - def get_context_data(self, **kwargs: Any) -> dict[str, Any]: - context = super().get_context_data(**kwargs) # type: ignore - user = self.request.user - context["can_edit"] = user.is_authenticated and user.is_staff # type: ignore - return context - - def get(self, request, *args, **kwargs): - if self.is_htmx(): - self.template_name = "web/partials/detail_event.htm" - return super().get(request, *args, **kwargs) - - class CreateEvent(CreateView): model = Event @@ -145,7 +59,7 @@ def get_success_url(self) -> str: "code of conduct.", extra_tags="safe", ) - return reverse("web:list_events") + return reverse("web:get_events") return super().get_success_url() @@ -157,119 +71,92 @@ class UpdateEvent(RequireStaffMixin, UpdateView): def get_success_url(self) -> str: if self.object.approved_at is None: - return reverse("web:list_events") + return reverse("web:get_events") return super().get_success_url() -class DetailTechGroup(HtmxViewMixin, DetailView): +class CreateTechGroup(RequireStaffMixin, CreateView): model = TechGroup + form_class = forms.TechGroupForm - def __init__(self, **kwargs: Any) -> None: - self.queryset = TechGroup.objects.prefetch_related( - Prefetch("event_set", Event.objects.filter(date_time__gte=timezone.localtime())) - ) - super().__init__(**kwargs) - def get_context_data(self, **kwargs: Any) -> dict[str, Any]: - context = super().get_context_data(**kwargs) - user = self.request.user - context["can_edit"] = user.is_authenticated and user.is_staff # type: ignore - return context +class UpdateTechGroup(RequireStaffMixin, UpdateView): + model = TechGroup + form_class = forms.TechGroupForm - def get(self, request, *args, **kwargs): - if self.is_htmx(): - self.template_name = "web/partials/detail_tech_group.htm" - return super().get(request, *args, **kwargs) +class EventCalendarView(HtmxCalendarView): + """Render a monthly calendar view of events""" -class ListTechGroup(CanEditMixin, HtmxViewMixin, HandyHelperListView): - title = "Tech Groups" - base_template = "spokanetech/base.html" - template_name = "web/techgroup_list.html" + title = "Spokane Tech Event Calendar" + event_model = Event + event_model_date_field = "date_time" + event_detail_url = "web:techevent_modal" + htmx_template_name = "web/partials/custom/calendar.htm" + template_name = "web/full/custom/calendar.html" - def __init__(self, **kwargs: Any) -> None: - self.queryset = TechGroup.objects.filter(enabled=True) - super().__init__(**kwargs) - def get(self, request, *args, **kwargs): - if self.is_htmx(): - self.template_name = "web/partials/techgroup_list.htm" - return super().get(request, *args, **kwargs) +class GetAboutContent(HtmxOptionView): + """Render the 'about' page""" + htmx_template_name = "web/partials/custom/about.htm" + template_name = "web/full/custom/about.html" -class CreateTechGroup(RequireStaffMixin, CreateView): - model = TechGroup - form_class = forms.TechGroupForm +class GetIndexContent(HtmxOptionView): + """Render the index page""" -class UpdateTechGroup(RequireStaffMixin, UpdateView): - model = TechGroup - form_class = forms.TechGroupForm + htmx_template_name = "web/partials/custom/index.htm" + template_name = "web/full/custom/index.html" -class BuildSidebar(BuildModelSidebarNav): - """Get a list of upcoming Events and enabled TechGroups and render a partial to use on the sidebar navigation""" +class GetTechEvent(HtmxOptionDetailView): + """Get details of an Event instance""" - template_name = "spokanetech/htmx/build_sidebar.htm" + model = Event + htmx_template_name = "web/partials/detail/event.htm" + template_name = "web/full/detail/event.html" - menu_item_list = [ - { - "queryset": Event.objects.filter(date_time__gte=timezone.localtime()).order_by("date_time"), - "list_all_url": reverse_lazy("web:list_events"), - "icon": """""", - }, - { - "queryset": TechGroup.objects.filter(enabled=True).order_by("name"), - "list_all_url": reverse_lazy("web:list_tech_groups"), - "icon": """""", - }, - ] +class GetTechEvents(HtmxOptionMultiFilterView): + """Get a list of Event entries""" -class GetEventDetailsModal(BuildBootstrapModalView): - """get details of an event and display in a modal""" + template_name = "web/full/list/events.html" + htmx_index_template_name = "web/partials/marquee/event_cards.htm" + htmx_list_template_name = "web/partials/list/events.htm" + queryset = Event.objects.filter(date_time__gte=timezone.now()) - modal_button_submit = None - modal_title = "Event Details" - def get(self, request, *args, **kwargs): - context = {} - context["object"] = Event.objects.get(pk=kwargs["pk"]) - self.modal_subtitle = context["object"] - self.modal_body = loader.render_to_string("web/partials/modal/detail_event.htm", context=context) - return super().get(request, *args, **kwargs) +class GetTechEventModal(ModelDetailBootstrapModalView): + """Get details of an Event instance and display in a modal""" + modal_button_submit = None + modal_title = "Event Info" + modal_template = "web/partials/modal/event_information.htm" + model = Event -class EventCalendarView(CalendarView): - """Render a monthly calendar view of events""" - title = "Spokane Tech Event Calendar" - event_model = Event - event_model_date_field = "date_time" - event_detail_url = "web:get_event_details" +class GetTechGroup(HtmxOptionDetailView): + """Get details of a TechGroup instance""" + model = TechGroup + htmx_template_name = "web/partials/detail/group.htm" + template_name = "web/full/detail/group.html" -class FilterListView(View): - """apply filters, as provided via queryparameters, to a list view that uses the FilterByQueryParamsMixin""" - def post(self, request, *args, **kwargs): - """process POST request +class GetTechGroups(HtmxOptionMultiFilterView): + """Get a list of TechGroup entries""" - **This handles multiple values for the same filter key - which Handy Helpers does not currently do.** - """ - redirect_url = self.request.META["HTTP_REFERER"] - form_parameters = self.request.POST + template_name = "web/full/list/groups.html" + htmx_index_template_name = "web/partials/marquee/group_cards.htm" + htmx_list_template_name = "web/partials/list/groups.htm" + queryset = TechGroup.objects.filter(enabled=True) - # build filtered URL - filter_url = f"{redirect_url.split('?')[0]}?" - for key in form_parameters: - # remove csrf token from POST parameters - if key == "csrfmiddlewaretoken": - continue - values = form_parameters.getlist(key) - for value in values: - filter_url += f"{key}={value}&" +class GetTechGroupModal(ModelDetailBootstrapModalView): + """get details of a TechGroup instance and display in a modal""" - return redirect(filter_url) + modal_button_submit = None + modal_title = "Group Info" + modal_template = "web/partials/modal/group_information.htm" + model = TechGroup