Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions base/components/components.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,25 @@
from base.templatetags.base_templatetags import querystring


@register("icon")
class Icon(Component):

class Kwargs(BaseModel):
kind: Literal["heart", "bookmark"]
color: str
label: str

template_file = "icon.html"

def get_template_data(self, args, kwargs, slots, context):
return {
"kind": kwargs.kind,
"label": kwargs.label,
"color": kwargs.color,
"classes": "w-[18px] h-[18px]",
}


class PaginationItem(BaseModel):
kind: Literal["current", "ellipsis", "number"]
text: Optional[str | int] = None
Expand Down
5 changes: 5 additions & 0 deletions base/components/icon.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{% if kind == 'heart' %}
<svg {% html_attrs aria-label=label class=classes fill=color %} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640"><!--!Font Awesome Free v7.0.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.--><path d="M305 151.1L320 171.8L335 151.1C360 116.5 400.2 96 442.9 96C516.4 96 576 155.6 576 229.1L576 231.7C576 343.9 436.1 474.2 363.1 529.9C350.7 539.3 335.5 544 320 544C304.5 544 289.2 539.4 276.9 529.9C203.9 474.2 64 343.9 64 231.7L64 229.1C64 155.6 123.6 96 197.1 96C239.8 96 280 116.5 305 151.1z"/></svg>
{% elif kind == 'bookmark' %}
<svg {% html_attrs aria-label=label class=classes fill=color %} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640"><!--!Font Awesome Free v7.0.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.--><path d="M192 64C156.7 64 128 92.7 128 128L128 544C128 555.5 134.2 566.2 144.2 571.8C154.2 577.4 166.5 577.3 176.4 571.4L320 485.3L463.5 571.4C473.4 577.3 485.7 577.5 495.7 571.8C505.7 566.1 512 555.5 512 544L512 128C512 92.7 483.3 64 448 64L192 64z"/></svg>
{% endif %}
31 changes: 31 additions & 0 deletions base/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
from .pagination import PAGE_VAR, Pagination


class ObjectList:
pagination_class = Pagination

def __init__(self, request, model, queryset, list_per_page):
self.model = model
self.queryset = queryset
self.list_per_page = list_per_page
self.params = dict(request.GET.lists())
if PAGE_VAR in self.params:
del self.params[PAGE_VAR]
self.result_objects = self.get_objects(request)

def __iter__(self):
return iter(self.result_objects)

def paginate(self, request, queryset):
pagination = self.pagination_class(
request,
self.model,
queryset,
self.list_per_page,
)
self.pagination = pagination
return pagination.get_objects()

def get_objects(self, request):
paginate_result = self.paginate(request, self.queryset)
return paginate_result
19 changes: 19 additions & 0 deletions cab/components/components.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
from django_components import Component, register
from pydantic import BaseModel

from base.main import ObjectList


@register("snippet_list")
class SnippetListComponent(Component):

template_file = "snippet_list.html"

class Kwargs(BaseModel):
snippet_list: ObjectList
model_config = {"arbitrary_types_allowed": True}

def get_template_data(self, args, kwargs, slots, context):
return {
"snippet_list": kwargs.snippet_list.result_objects,
}
46 changes: 46 additions & 0 deletions cab/components/snippet_list.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<section aria-labelledby="snippet-list-title">
<h2 id="snippet-list-title" class="sr-only">Snippet List</h2>
<div class="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 auto-rows-fr gap-8">
{% for snippet in snippet_list %}
<article class="flex flex-col flex-nowrap shadow transition-all duration-500 font-common hover:-translate-y-2 hover:shadow-xl">
<header class="flex flex-auto flex-col p-4 mb-0 bg-transparent">
<h3 class="my-2 bg-none truncate"><a class="text-2xl font-title text-base-black no-underline font-bold" href="{{ snippet.get_absolute_url }}" title="{{ snippet.title }}">{{ snippet.title }}</a></h3>
<p class="line-clamp-6 my-6 font-text text-base">{{ snippet.description }}</p>
<div class="mt-auto">
<ul aria-label="tag list" class="m-0 my-2 inline-flex gap-2 flex-wrap list-none text-sm">
{% for tag in snippet.tags.all %}
<li class="py-1.5 px-2 border-2 text-base-white-400 bg-base-green-400 border-base-green-400 rounded-lg">{{ tag.name }}</li>
{% endfor %}
</ul>
<div class="my-2 flex justify-between">
<time aria-label="updated date">{{ snippet.updated_date|date:'D d M Y' }}</time>
<a class="inline-flex items-center gap-2 cursor-pointer group underline underline-offset-4" href="{{ snippet.get_absolute_url }}">
<span class="transition-colors">Read More</span>
<svg class="w-6 h-6 group-hover:text-base-green-400" viewBox="0 0 24 24" fill="none">
<line class="stroke-current stroke-2 -translate-x-2 opacity-0 group-hover:translate-x-0 group-hover:opacity-100 transition-all duration-300" x1="4" y1="12" x2="16" y2="12"/>
<path class="stroke-current stroke-2 fill-none stroke-linecap-round stroke-linejoin-round group-hover:translate-x-1 transition-transform duration-300" d="M15 8l4 4-4 4"/>
</svg>
</a>
</div>
</div>
</header>
<footer class="p-4 flex items-center justify-between border-t-1 border-gray-300 before:content-none after:content-none">
<div>
<strong>Author: </strong>
<a href="{{ snippet.author.get_absolute_url }}" class="underline underline-offset-4 hover:text-base-orange-400">{{ snippet.author.username }}</a>
</div>
<ul class="flex m-0 gap-4 font-text">
<li class="flex items-center gap-2">
{% component 'icon' kind='heart' label='rating' color='red' / %}
<span>{{ snippet.rating_score }}</span>
</li>
<li class="flex items-center gap-2">
{% component 'icon' kind='bookmark' label='bookmark' color='#ffd700' / %}
<span>{{ snippet.bookmark_count }}</span>
</li>
</ul>
</footer>
</article>
{% endfor %}
</div>
</section>
15 changes: 11 additions & 4 deletions cab/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from django.utils.safestring import mark_safe
from markdown import markdown as markdown_func

from base.main import ObjectList
from base.pagination import Pagination


Expand Down Expand Up @@ -35,23 +36,29 @@ def object_list(
hits
number of objects, total
"""
from cab.models import Snippet

if extra_context is None:
extra_context = {}
queryset = queryset._clone()
model = queryset.model
opts = model._meta
if paginate_by:
pagination = Pagination(request, model, queryset, paginate_by)
object_list = pagination.get_objects()
if queryset.model == Snippet:
object_list = ObjectList(request, queryset.model, queryset, 15)
pagination = object_list.pagination
else:
Comment on lines +48 to +50
Copy link
Preview

Copilot AI Jul 31, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The hardcoded value 15 should use the paginate_by parameter instead. This magic number makes the code inconsistent and ignores the function's pagination configuration.

Suggested change
object_list = ObjectList(request, queryset.model, queryset, 15)
pagination = object_list.pagination
else:
paginate_by = paginate_by or 15 # Default to 15 if paginate_by is None
object_list = ObjectList(request, queryset.model, queryset, paginate_by)
pagination = object_list.pagination

Copilot uses AI. Check for mistakes.

pagination = Pagination(request, model, queryset, paginate_by)
object_list = pagination.get_objects()

context = {
"%s_list" % template_object_name: object_list,
"object_list": object_list,
"pagination": pagination,
"hits": pagination.result_count,
}
else:
context = {
"%s_list" % template_object_name: object_list,
"object_list": queryset,
Comment on lines +55 to +61
Copy link
Preview

Copilot AI Jul 31, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The context key is hardcoded to 'object_list', but the template_object_name parameter suggests this should be configurable. This breaks the function's contract and could cause template variable name mismatches.

Copilot uses AI. Check for mistakes.

Comment on lines +55 to +61
Copy link
Preview

Copilot AI Jul 31, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The context key is hardcoded to 'object_list', but the template_object_name parameter suggests this should be configurable. This breaks the function's contract and could cause template variable name mismatches.

Copilot uses AI. Check for mistakes.

}
if not allow_empty and len(queryset) == 0:
raise Http404
Expand Down
11 changes: 8 additions & 3 deletions djangosnippets/templates/base.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@
<link rel="shortcut icon" type="image/x-icon" href="{% static "img/favicon.ico" %}" />
<link rel="stylesheet" href="//netdna.bootstrapcdn.com/font-awesome/4.2.0/css/font-awesome.css">
<link rel="stylesheet" href="//ajax.googleapis.com/ajax/libs/jqueryui/1.11.1/themes/smoothness/jquery-ui.css" />
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Nunito:ital,wght@0,200..1000;1,200..1000&family=Playfair+Display:ital,wght@0,400..900;1,400..900&family=Raleway:ital,wght@0,100..900;1,100..900&display=swap" rel="stylesheet">
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@chriswedgwood
I added three google fonts, there shouldn’t be any issues right?
If there are any copyright related concerns, who should I ask to review them?

<link rel="alternate" href="{% url 'cab_feed_latest' %}" type="application/atom+xml" title="Feed of latest snippets" />
<link rel="stylesheet" href="{% static "css/main.css" %}" type="text/css" />
{% block feeds %}{% endblock %}
Expand Down Expand Up @@ -35,7 +38,7 @@
</ul>
</nav>
</div></header>
<div id="main">
<div id="main" class="container mb-auto mx-auto w-7/8">
{% block secondary_nav %}
<nav id="subnav">
<ul>
Expand All @@ -59,14 +62,16 @@

<div id="base-container">
<h1>{% block content_header %}{% endblock %}</h1>
<div id="content">
{% with current_url_name=request.resolver_match.url_name %}
<div id="content" class="{% if current_url_name == 'home' %}w-1/2{% else %}w-full{% endif %}">
{% block content %}
{% endblock %}
</div>
<div id="sidebar">
<div id="sidebar" class="{% if current_url_name == 'home' %}w-1/2{% else %}hidden md:block md:w-1/4{% endif %}">
{% block sidebar %}
{% endblock %}
</div>
{% endwith %}
{% block extra_content %}
{% endblock %}
</div>
Expand Down
55 changes: 17 additions & 38 deletions djangosnippets/templates/cab/partials/most_bookmarked.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,32 +4,11 @@

<h1>Most bookmarked snippets{% if months %} last {{ months }} months{% endif %}</h1>

<div id="content">
<div id="content" class="w-full">


{% if object_list %}
<table class="snippet_list">
<thead>
<tr>
<th>Title</th>
<th>Author</th>
<th>Tags</th>
<th>Publication date</th>
<th>Rating</th>
</tr>
</thead>
<tbody>
{% for snippet in object_list %}
<tr>
<td><a href="{{ snippet.get_absolute_url }}">{% if not snippet.title|strip %}Untitled{% else %}{{ snippet.title }}{% endif %}</a></td>
<td><a href="{{ snippet.author.get_absolute_url }}">{{ snippet.author.username }}</a></td>
<td>{% for tag in snippet.tags.all %}<a href="{% url 'cab_snippet_matches_tag' tag.slug %}">{{ tag.name }}</a> {% endfor %}</td>
<td>{{ snippet.pub_date }}</td>
<td><span class="rating-{% if snippet.rating_score >= 0 %}positive{% else %}negative{% endif %}">{% if snippet.rating_score >= 0 %}+{% endif %}{{ snippet.rating_score }}</span></td>
</tr>
{% endfor %}
</tbody>
</table>
{% component 'snippet_list' snippet_list=object_list / %}
{% component "pagination" pagination_obj=pagination / %}
<p class="count">{{ hits }} snippet{{ hits|pluralize }} posted so far.</p>
{% else %}
Expand All @@ -38,18 +17,18 @@ <h1>Most bookmarked snippets{% if months %} last {{ months }} months{% endif %}<
</div>


<div id="sidebar">
<p>You're looking at the most-bookmarked snippets on the site; {% if user.is_authenticated %}you can help useful snippets show up here by clicking the "bookmark this snippet" link on their pages and ading them to <a href="{% url 'cab_user_bookmarks' %}">your bookmarks</a>{% else %}if you'd like to help useful snippets show up here, <a href="{% url 'account_login' %}">sign up for an account</a> and you'll get your own bookmarks list{% endif %}.</p>

<nav class="filter">
<h3>Filter by date</h3>
<ul>
<li{% if not months %} class="active"{% endif %}><a href="{{ request.path }}">Any time</a></li>
<li{% if months == 3 %} class="active"{% endif %}><a href="{{ request.path }}?months=3">3 months</a></li>
<li{% if months == 6 %} class="active"{% endif %}><a href="{{ request.path }}?months=6">6 months</a></li>
<li{% if months == 12 %} class="active"{% endif %}><a href="{{ request.path }}?months=12">1 year</a></li>
</ul>
</nav>

<p><a rel="alternate" href="{% url 'cab_feed_latest' %}" type="application/atom+xml"><i class="fa fa-fw fa-rss-square"></i>Feed of latest snippets</a></p>
</div>
{#<div id="sidebar" class="hidden md:block md:w-1/4">#}
{#<p>You're looking at the most-bookmarked snippets on the site; {% if user.is_authenticated %}you can help useful snippets show up here by clicking the "bookmark this snippet" link on their pages and ading them to <a href="{% url 'cab_user_bookmarks' %}">your bookmarks</a>{% else %}if you'd like to help useful snippets show up here, <a href="{% url 'account_login' %}">sign up for an account</a> and you'll get your own bookmarks list{% endif %}.</p>#}
{##}
{# <nav class="filter">#}
{# <h3>Filter by date</h3>#}
{# <ul>#}
{# <li{% if not months %} class="active"{% endif %}><a href="{{ request.path }}">Any time</a></li>#}
{# <li{% if months == 3 %} class="active"{% endif %}><a href="{{ request.path }}?months=3">3 months</a></li>#}
{# <li{% if months == 6 %} class="active"{% endif %}><a href="{{ request.path }}?months=6">6 months</a></li>#}
{# <li{% if months == 12 %} class="active"{% endif %}><a href="{{ request.path }}?months=12">1 year</a></li>#}
{# </ul>#}
{# </nav>#}
{##}
{# <p><a rel="alternate" href="{% url 'cab_feed_latest' %}" type="application/atom+xml"><i class="fa fa-fw fa-rss-square"></i>Feed of latest snippets</a></p>#}
{#</div>#}
2 changes: 1 addition & 1 deletion djangosnippets/templates/cab/partials/tag_list.html
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@

{% load core_tags %}
<h1>All tags</h1>
<div id="content">
<div id="content" class="w-full md:w-3/4">
{% if object_list %}
<ul>
{% for tag in object_list %}
Expand Down
57 changes: 18 additions & 39 deletions djangosnippets/templates/cab/partials/top_rated.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,50 +4,29 @@


<h1>Top-rated snippets{% if months %} last {{ months }} months{% endif %}</h1>
<div id="content">
<div id="content" class="w-full">
{% if object_list %}
<table class="snippet_list">
<thead>
<tr>
<th>Title</th>
<th>Author</th>
<th>Tags</th>
<th>Publication date</th>
<th>Rating</th>
</tr>
</thead>
<tbody>
{% for snippet in object_list %}
<tr>
<td><a href="{{ snippet.get_absolute_url }}">{% if not snippet.title|strip %}Untitled{% else %}{{ snippet.title }}{% endif %}</a></td>
<td><a href="{{ snippet.author.get_absolute_url }}">{{ snippet.author.username }}</a></td>
<td>{% for tag in snippet.tags.all %}<a href="{% url 'cab_snippet_matches_tag' tag.slug %}">{{ tag.name }}</a> {% endfor %}</td>
<td>{{ snippet.pub_date }}</td>
<td><span class="rating-{% if snippet.rating_score >= 0 %}positive{% else %}negative{% endif %}">{% if snippet.rating_score >= 0 %}+{% endif %}{{ snippet.rating_score }}</span></td>
</tr>
{% endfor %}
</tbody>
</table>
{% component 'snippet_list' snippet_list=object_list / %}
{% component "pagination" pagination_obj=pagination / %}
<p class="count">{{ hits }} snippet{{ hits|pluralize }} posted so far.</p>
{% else %}
<p class="empty">No snippets posted yet.</p>
{% endif %}
</div>

<div id="sidebar">
<p>You're looking at the top-rated snippets currently on the site; {% if user.is_authenticated %}if you'd like to contribute, just click the ratings link in the sidebar of a particular snippet's page; you can rate any snippet "useful" or "not useful"{% else %}if you'd like to contribute, <a href="{% url 'account_login' %}">sign up for an account</a> and you'll be able to rate any snippet you see{% endif %}.</p>

<nav class="filter">
<h3>Filter by date</h3>
<ul>
<li{% if not months %} class="active"{% endif %}><a href="{{ request.path }}">Any time</a></li>
<li{% if months == 3 %} class="active"{% endif %}><a href="{{ request.path }}?months=3">3 months</a></li>
<li{% if months == 6 %} class="active"{% endif %}><a href="{{ request.path }}?months=6">6 months</a></li>
<li{% if months == 12 %} class="active"{% endif %}><a href="{{ request.path }}?months=12">1 year</a></li>
</ul>
</nav>

<p><a rel="alternate" href="{% url 'cab_feed_latest' %}" type="application/atom+xml"><i class="fa fa-fw fa-rss-square"></i>Feed of latest snippets</a></p>

</div>
{#<div id="sidebar" class="hidden md:block md:w-1/4">#}
{#<p>You're looking at the top-rated snippets currently on the site; {% if user.is_authenticated %}if you'd like to contribute, just click the ratings link in the sidebar of a particular snippet's page; you can rate any snippet "useful" or "not useful"{% else %}if you'd like to contribute, <a href="{% url 'account_login' %}">sign up for an account</a> and you'll be able to rate any snippet you see{% endif %}.</p>#}
{##}
{#<nav class="filter">#}
{# <h3>Filter by date</h3>#}
{# <ul>#}
{# <li{% if not months %} class="active"{% endif %}><a href="{{ request.path }}">Any time</a></li>#}
{# <li{% if months == 3 %} class="active"{% endif %}><a href="{{ request.path }}?months=3">3 months</a></li>#}
{# <li{% if months == 6 %} class="active"{% endif %}><a href="{{ request.path }}?months=6">6 months</a></li>#}
{# <li{% if months == 12 %} class="active"{% endif %}><a href="{{ request.path }}?months=12">1 year</a></li>#}
{# </ul>#}
{#</nav>#}
{##}
{#<p><a rel="alternate" href="{% url 'cab_feed_latest' %}" type="application/atom+xml"><i class="fa fa-fw fa-rss-square"></i>Feed of latest snippets</a></p>#}
{##}
{#</div>#}
Comment on lines +17 to +32
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The date filter has been commented out.
It will likely be removed.
Later, more detailed filtering options will be provided through a slide-in panel,
and for the date filter, I think it would be better to add two input fields and offer it in a between format.
I will also create a related issue for this.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Issue -> #595

Loading