diff --git a/akarpov/about/__init__.py b/akarpov/about/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/akarpov/about/admin.py b/akarpov/about/admin.py new file mode 100644 index 0000000..90aad10 --- /dev/null +++ b/akarpov/about/admin.py @@ -0,0 +1,5 @@ +from django.contrib import admin + +from akarpov.about.models import Project + +admin.site.register(Project) diff --git a/akarpov/about/apps.py b/akarpov/about/apps.py new file mode 100644 index 0000000..8fda74a --- /dev/null +++ b/akarpov/about/apps.py @@ -0,0 +1,7 @@ +from django.apps import AppConfig +from django.utils.translation import gettext_lazy as _ + + +class AboutConfig(AppConfig): + name = "akarpov.about" + verbose_name = _("About me app") diff --git a/akarpov/about/migrations/0001_initial.py b/akarpov/about/migrations/0001_initial.py new file mode 100644 index 0000000..74f097a --- /dev/null +++ b/akarpov/about/migrations/0001_initial.py @@ -0,0 +1,49 @@ +# Generated by Django 4.1.7 on 2023-03-24 07:12 + +from django.db import migrations, models +import django_extensions.db.fields + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [] + + operations = [ + migrations.CreateModel( + name="Project", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "created", + django_extensions.db.fields.CreationDateTimeField( + auto_now_add=True, verbose_name="created" + ), + ), + ( + "modified", + django_extensions.db.fields.ModificationDateTimeField( + auto_now=True, verbose_name="modified" + ), + ), + ("name", models.CharField(max_length=30)), + ("description", models.TextField(blank=True)), + ("md", models.TextField(blank=True)), + ("image", models.ImageField(upload_to="uploads/images/")), + ("source_link", models.URLField(blank=True)), + ("view_link", models.URLField(blank=True)), + ], + options={ + "ordering": ("-modified",), + }, + ), + ] diff --git a/akarpov/about/migrations/__init__.py b/akarpov/about/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/akarpov/about/models.py b/akarpov/about/models.py new file mode 100644 index 0000000..762810a --- /dev/null +++ b/akarpov/about/models.py @@ -0,0 +1,21 @@ +from django.db import models +from django.urls import reverse +from django_extensions.db.models import TimeStampedModel + + +class Project(TimeStampedModel): + name = models.CharField(max_length=30) + description = models.TextField(blank=True) + md = models.TextField(blank=True) + image = models.ImageField(upload_to="uploads/images/", blank=False) + source_link = models.URLField(blank=True) + view_link = models.URLField(blank=True) + + def get_absolute_url(self): + return reverse("about:project", kwargs={"pk": self.pk}) + + def __str__(self): + return self.name + + class Meta: + ordering = ("-modified",) diff --git a/akarpov/about/urls.py b/akarpov/about/urls.py new file mode 100644 index 0000000..bf7f24e --- /dev/null +++ b/akarpov/about/urls.py @@ -0,0 +1,10 @@ +from django.urls import path + +from . import views + +app_name = "about" + +urlpatterns = [ + path("", views.about_view, name="about"), + path("", views.project_view, name="project"), +] diff --git a/akarpov/about/views.py b/akarpov/about/views.py new file mode 100644 index 0000000..21361a0 --- /dev/null +++ b/akarpov/about/views.py @@ -0,0 +1,26 @@ +from django.views.generic import DetailView, TemplateView + +from akarpov.about.models import Project + + +class AboutView(TemplateView): + template_name = "about/about.html" + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context["projects"] = Project.objects.all() + return context + + +about_view = AboutView.as_view() + + +class ProjectView(DetailView): + model = Project + slug_field = "pk" + slug_url_kwarg = "pk" + + template_name = "about/project.html" + + +project_view = ProjectView.as_view() diff --git a/akarpov/blog/admin.py b/akarpov/blog/admin.py index b48fc6e..244884a 100644 --- a/akarpov/blog/admin.py +++ b/akarpov/blog/admin.py @@ -2,9 +2,43 @@ from akarpov.blog.models import Post, PostRating, Tag -# Register your models here. + +class PostAdmin(admin.ModelAdmin): + list_display = ("title", "post_views") + list_filter = ("creator",) + readonly_fields = ("created", "edited") + fieldsets = ( + (None, {"fields": ("title", "body", "creator", "tags")}), + ( + "Ratings", + { + "classes": ("collapse",), + "fields": ( + "post_views", + "rating", + "rating_up", + "rating_down", + "comment_count", + ), + }, + ), + ( + "Images", + { + "classes": ("collapse",), + "fields": ("image", "image_cropped"), + }, + ), + ( + "Urls", + { + "classes": ("collapse",), + "fields": ("slug", "short_link"), + }, + ), + ) -admin.site.register(Post) +admin.site.register(Post, PostAdmin) admin.site.register(Tag) admin.site.register(PostRating) diff --git a/akarpov/blog/migrations/0005_alter_post_slug.py b/akarpov/blog/migrations/0005_alter_post_slug.py new file mode 100644 index 0000000..2505e1d --- /dev/null +++ b/akarpov/blog/migrations/0005_alter_post_slug.py @@ -0,0 +1,18 @@ +# Generated by Django 4.1.7 on 2023-03-24 08:51 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("blog", "0004_alter_post_short_link"), + ] + + operations = [ + migrations.AlterField( + model_name="post", + name="slug", + field=models.SlugField(blank=True, max_length=20, unique=True), + ), + ] diff --git a/akarpov/blog/models.py b/akarpov/blog/models.py index f66c9b1..aa5b912 100644 --- a/akarpov/blog/models.py +++ b/akarpov/blog/models.py @@ -1,7 +1,7 @@ from ckeditor_uploader.fields import RichTextUploadingField from colorfield.fields import ColorField from django.db import models -from django.db.models import Count, SlugField +from django.db.models import Count from django.urls import reverse from akarpov.common.models import BaseImageModel @@ -15,7 +15,6 @@ class Post(BaseImageModel, ShortLink): body = RichTextUploadingField(blank=False) creator = models.ForeignKey(User, on_delete=models.CASCADE, related_name="posts") - slug = SlugField(max_length=20, blank=True) post_views = models.IntegerField(default=0) rating = models.IntegerField(default=0) @@ -66,6 +65,9 @@ def get_absolute_url(self): class Meta: ordering = ["-created"] + class SlugMeta: + slug_length = 3 + class Tag(models.Model): name = models.CharField(max_length=20, unique=True) diff --git a/akarpov/blog/signals.py b/akarpov/blog/signals.py index 6239da3..0dd5b62 100644 --- a/akarpov/blog/signals.py +++ b/akarpov/blog/signals.py @@ -3,17 +3,12 @@ from akarpov.blog.models import Post, PostRating, Tag from akarpov.common.tasks import crop_model_image -from akarpov.utils.generators import generate_charset, generate_hex_color +from akarpov.utils.generators import generate_hex_color @receiver(pre_save, sender=Post) def post_on_save(sender, instance: Post, **kwargs): - if instance.id is None: - slug = generate_charset(3) - while Post.objects.filter(slug=slug).exists(): - slug = generate_charset(3) - instance.slug = slug - else: + if instance.id: previous = Post.objects.get(id=instance.id) if ( previous.image != instance.image diff --git a/akarpov/common/models.py b/akarpov/common/models.py index 89f7e34..f3c020c 100644 --- a/akarpov/common/models.py +++ b/akarpov/common/models.py @@ -1,6 +1,7 @@ from django.db import models from akarpov.utils.files import user_file_upload_mixin +from akarpov.utils.generators import generate_charset class BaseImageModel(models.Model): @@ -9,3 +10,54 @@ class BaseImageModel(models.Model): class Meta: abstract = True + + +def create_model_slug(sender, instance, **kwargs): + def _generate_charset(): + if private: + return generate_charset(private_slug_length) + return generate_charset(slug_length) + + if instance.id is None: + model = sender + slug_length = 5 + private_slug_length = 20 + private = False + + if hasattr(model, "SlugMeta"): + if hasattr(model.SlugMeta, "slug_length"): + slug_length = model.SlugMeta.slug_length + if hasattr(model.SlugMeta, "private_slug_length"): + private_slug_length = model.SlugMeta.private_slug_length + if hasattr(instance, "private"): + if instance.private: + private = True + if hasattr(instance, "public"): + if not instance.public: + private = True + + slug = _generate_charset() + while model.objects.filter(slug=slug).exists(): + slug = _generate_charset() + instance.slug = slug + + +class SlugModel(models.Model): + """ + model to store and generate slug for model instances + for custom slug length use: slug_length, private_slug_length Meta options + """ + + slug = models.SlugField(max_length=20, blank=True, unique=True) + + @classmethod + def __init_subclass__(cls, **kwargs): + super().__init_subclass__(**kwargs) + models.signals.pre_save.connect(create_model_slug, sender=cls) + + class Meta: + abstract = True + + class SlugMeta: + slug_length = 5 + private_slug_length = 20 diff --git a/akarpov/common/signals.py b/akarpov/common/signals.py new file mode 100644 index 0000000..e69de29 diff --git a/akarpov/files/migrations/0006_alter_basefile_slug.py b/akarpov/files/migrations/0006_alter_basefile_slug.py new file mode 100644 index 0000000..2d8ffd3 --- /dev/null +++ b/akarpov/files/migrations/0006_alter_basefile_slug.py @@ -0,0 +1,18 @@ +# Generated by Django 4.1.7 on 2023-03-24 08:51 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("files", "0005_alter_basefile_description_alter_basefile_folder_and_more"), + ] + + operations = [ + migrations.AlterField( + model_name="basefile", + name="slug", + field=models.SlugField(blank=True, max_length=20, unique=True), + ), + ] diff --git a/akarpov/files/models.py b/akarpov/files/models.py index d8a66fd..fbce8d1 100644 --- a/akarpov/files/models.py +++ b/akarpov/files/models.py @@ -20,7 +20,6 @@ class BaseFile(TimeStampedModel, ShortLink): name = CharField(max_length=100) description = TextField(blank=True) - slug = SlugField(max_length=20, blank=True) private = BooleanField(default=True) user = ForeignKey("users.User", related_name="files", on_delete=CASCADE) diff --git a/akarpov/files/signlas.py b/akarpov/files/signlas.py deleted file mode 100644 index cb88c7c..0000000 --- a/akarpov/files/signlas.py +++ /dev/null @@ -1,20 +0,0 @@ -from django.db.models.signals import pre_save -from django.dispatch import receiver - -from akarpov.files.models import BaseFile -from akarpov.utils.generators import generate_charset - - -@receiver(pre_save, sender=BaseFile) -def file_on_save(sender, instance: BaseFile, **kwargs): - if instance.id is None: - if instance.private: - slug = generate_charset(20) - while BaseFile.objects.filter(slug=slug).exists(): - slug = generate_charset(20) - else: - slug = generate_charset(5) - while BaseFile.objects.filter(slug=slug).exists(): - slug = generate_charset(5) - - instance.slug = slug diff --git a/akarpov/fixtures/projects.json b/akarpov/fixtures/projects.json new file mode 100644 index 0000000..1eff4e6 --- /dev/null +++ b/akarpov/fixtures/projects.json @@ -0,0 +1,352 @@ +[ +{ + "model": "about.project", + "pk": 1, + "fields": { + "name": "Blog app", + "description": "App to create, view, comment and rate posts", + "md": "", + "image": "uploads/images/E7BE3B27-5184-4C34-969D-D05E25DA69BE.jpeg", + "created": "2021-12-15T09:28:22Z", + "modified": "2022-05-17T21:00:00Z", + "source_link": "https://github.com/Alexander-D-Karpov/django-blog-app", + "view_link": "https://akarpov.ru/" + } +}, +{ + "model": "about.project", + "pk": 2, + "fields": { + "name": "User app", + "description": "App to handle user login, registration, setting and user profile. API also provided", + "md": "", + "image": "uploads/images/A185F924-5FDF-4580-8116-55FD575D273F.jpeg", + "created": "2021-12-15T09:36:34Z", + "modified": "2022-04-22T21:00:00Z", + "source_link": "https://github.com/Alexander-D-Karpov/django-user-app", + "view_link": "https://akarpov.ru/user/" + } +}, +{ + "model": "about.project", + "pk": 3, + "fields": { + "name": "File share app", + "description": "App to share files", + "md": "- upload files\r\n- manage allowed upload space for user\r\n- delete files\r\n- create groups\r\n- upload files to groups and share them", + "image": "uploads/images/Screenshot_20220513_130820.png", + "created": "2021-12-27T21:00:00Z", + "modified": "2022-05-16T21:00:00Z", + "source_link": "https://github.com/Alexander-D-Karpov/django-fileshare-app", + "view_link": "http://akarpov.ru/file/" + } +}, +{ + "model": "about.project", + "pk": 4, + "fields": { + "name": "About me app", + "description": "Page to share my links, stack and projects I made", + "md": "", + "image": "uploads/images/Screenshot_20220513_131404.png", + "created": "2022-02-01T21:00:00Z", + "modified": "2022-05-15T21:00:00Z", + "source_link": "https://github.com/Alexander-D-Karpov/about-me", + "view_link": "https://akarpov.ru/about" + } +}, +{ + "model": "about.project", + "pk": 5, + "fields": { + "name": "Image app", + "description": "Share your images", + "md": "- image lazy load\r\n- image optimization on backend\r\n- metadata extractor", + "image": "uploads/images/Screenshot_20220513_131716.png", + "created": "2022-04-18T21:00:00Z", + "modified": "2022-05-10T21:00:00Z", + "source_link": "https://github.com/Alexander-D-Karpov/django-image-app", + "view_link": "https://akarpov.ru/pics/" + } +}, +{ + "model": "about.project", + "pk": 6, + "fields": { + "name": "Forms generator", + "description": "Site to generate and share forms", + "md": "- modes available for now:\r\n- - Question\r\n- - Number\r\n- - Select", + "image": "uploads/images/Screenshot_20220513_132259.png", + "created": "2022-04-14T21:00:00Z", + "modified": "2022-05-15T21:00:00Z", + "source_link": "https://github.com/Alexander-D-Karpov/django-forms-app", + "view_link": "https://akarpov.ru/forms/" + } +}, +{ + "model": "about.project", + "pk": 7, + "fields": { + "name": "Music app", + "description": "App to handle music files, converting them to audio tracks", + "md": "- get list of songs\r\n- share song", + "image": "uploads/images/Screenshot_20220513_132850.png", + "created": "2022-02-07T21:00:00Z", + "modified": "2022-04-17T21:00:00Z", + "source_link": "https://github.com/Alexander-D-Karpov/django-music-app", + "view_link": "https://akarpov.ru/music/" + } +}, +{ + "model": "about.project", + "pk": 8, + "fields": { + "name": "CTF platform", + "description": "Site to view tasks, submit flags, view attempts and start time", + "md": "", + "image": "uploads/images/Screenshot_20220513_133358.png", + "created": "2021-12-22T21:00:00Z", + "modified": "2022-03-15T21:00:00Z", + "source_link": "https://github.com/Alexander-D-Karpov/django-ctf-app", + "view_link": "https://akarpov.ru/ctf/" + } +}, +{ + "model": "about.project", + "pk": 9, + "fields": { + "name": "Flower shop", + "description": "Site to create orders", + "md": "- create category\r\n- create product\r\n- get notification on order create from bot", + "image": "uploads/images/Screenshot_20220513_133815.png", + "created": "2022-04-05T21:00:00Z", + "modified": "2022-05-31T21:00:00Z", + "source_link": "", + "view_link": "https://sharoboom-livny.ru/" + } +}, +{ + "model": "about.project", + "pk": 10, + "fields": { + "name": "Tender hackathon backend", + "description": "Backend for tender platform", + "md": "- [Tender hack site](https://tenderhack.ru/)\r\n- get data and create it on various endpoints\r\n- connect to session with web sockets", + "image": "uploads/images/Screenshot_20220513_134426.png", + "created": "2022-04-14T21:00:00Z", + "modified": "2022-04-16T21:00:00Z", + "source_link": "https://github.com/Alexander-D-Karpov/tender_hack_back", + "view_link": "" + } +}, +{ + "model": "about.project", + "pk": 11, + "fields": { + "name": "Bot Polling Master", + "description": "A small telegram bot to handle messages from users on several bots, gets data from web server to synchronize with frontend", + "md": "", + "image": "uploads/images/Screenshot_20220513_140011.png", + "created": "2022-03-17T21:00:00Z", + "modified": "2022-03-19T21:00:00Z", + "source_link": "https://github.com/Alexander-D-Karpov/bot_polling_master", + "view_link": "" + } +}, +{ + "model": "about.project", + "pk": 12, + "fields": { + "name": "vkHandler", + "description": "bot to fetch data from vk, get tags and send posts to user subscribed to tags. Notifies user if post contains date in the morning of this day", + "md": "", + "image": "uploads/images/Screenshot_20220513_145339.png", + "created": "2022-02-11T21:00:00Z", + "modified": "2022-02-14T21:00:00Z", + "source_link": "https://github.com/Alexander-D-Karpov/vkHandler", + "view_link": "http://t.me/vkHand_bot" + } +}, +{ + "model": "about.project", + "pk": 13, + "fields": { + "name": "Image API", + "description": "Api to handle and store images. Crop image, provide user session as JWT", + "md": "", + "image": "uploads/images/photo_2022-01-23_23-54-26.jpg", + "created": "2022-01-23T21:00:00Z", + "modified": "2022-01-23T21:00:00Z", + "source_link": "https://github.com/Alexander-D-Karpov/image_api", + "view_link": "" + } +}, +{ + "model": "about.project", + "pk": 14, + "fields": { + "name": "Music server", + "description": "MPD server over http with function to load tracks from YouTube, Yandex music and Spotify with metadata gathering from Spotify and Yandex", + "md": "[Stream utl](http://music.akarpov.ru)", + "image": "uploads/images/Screenshot_20220923_000907.png", + "created": "2022-09-19T21:00:00Z", + "modified": "2022-09-28T21:00:00Z", + "source_link": "https://github.com/Alexander-D-Karpov/music_server", + "view_link": "http://music.akarpov.ru" + } +}, +{ + "model": "about.project", + "pk": 15, + "fields": { + "name": "Block site generator", + "description": "Backend for constructor sites. Create sites, blocks and build block hierarchy", + "md": "", + "image": "uploads/images/photo_2022-08-18_23-00-40.jpg", + "created": "2022-08-12T21:00:00Z", + "modified": "2022-08-21T21:00:00Z", + "source_link": "https://github.com/Alexander-D-Karpov/agora_hack_backend", + "view_link": "" + } +}, +{ + "model": "about.project", + "pk": 16, + "fields": { + "name": "RPG backend", + "description": "Chess RPG game backend with web sockets, timeouts and API", + "md": "", + "image": "uploads/images/image.png", + "created": "2022-06-03T21:00:00Z", + "modified": "2022-07-31T21:00:00Z", + "source_link": "https://github.com/evgen-app/chess_rpg_backend", + "view_link": "" + } +}, +{ + "model": "about.project", + "pk": 17, + "fields": { + "name": "NLP DOC processer", + "description": "Backend for service to process docx, doc and other files to check if it follows standards.", + "md": "", + "image": "uploads/images/photo_2022-08-28_04-13-48.jpg", + "created": "2022-08-25T21:00:00Z", + "modified": "2022-08-27T21:00:00Z", + "source_link": "https://github.com/Ai-hack-MAGNUM-OPUS/backend", + "view_link": "" + } +}, +{ + "model": "about.project", + "pk": 18, + "fields": { + "name": "Motivational platform", + "description": "Website to connect workers in teams and reward with cryptocurrency", + "md": "", + "image": "uploads/images/photo_2022-10-09_10-49-55.jpg", + "created": "2022-10-07T21:00:00Z", + "modified": "2022-10-11T21:00:00Z", + "source_link": "https://github.com/more-tech4-magnum-opus", + "view_link": "" + } +}, +{ + "model": "about.project", + "pk": 19, + "fields": { + "name": "Product search engine", + "description": "API to handle and help search queries", + "md": "", + "image": "uploads/images/photo_2022-10-22_22-40-48.jpg", + "created": "2022-10-20T21:00:00Z", + "modified": "2022-10-22T21:00:00Z", + "source_link": "https://github.com/magnum-opus-tender-hack/backend", + "view_link": "" + } +}, +{ + "model": "about.project", + "pk": 20, + "fields": { + "name": "DICOM edit platform", + "description": "complex platform to view dicom files and generate pathologies", + "md": "", + "image": "uploads/images/photo_2022-11-17_23-54-35.jpg", + "created": "2022-10-24T21:00:00Z", + "modified": "2022-11-05T21:00:00Z", + "source_link": "https://github.com/leaders-of-digital-9-task/backend", + "view_link": "" + } +}, +{ + "model": "about.project", + "pk": 21, + "fields": { + "name": "Med Manage Platform", + "description": "App with wide functionality to create forms, manage patient info, create appointments, etc", + "md": "", + "image": "uploads/images/photo_2023-02-21_13-15-28.jpg", + "created": "2022-12-08T21:00:00Z", + "modified": "2022-12-08T21:00:00Z", + "source_link": "https://github.com/Alexander-D-Karpov/med_backend", + "view_link": "" + } +}, +{ + "model": "about.project", + "pk": 22, + "fields": { + "name": "cookiecutter-django", + "description": "My django cookiecutter for fast creation of django projects specified for API development.", + "md": "Features:\r\n\r\n - DRF\r\n - Poetry\r\n - Docker\r\n - CI\r\n - Celery\r\n - Structlog\r\n - Useful functions and auth", + "image": "uploads/images/photo_2023-02-21_13-28-05.jpg", + "created": "2023-01-06T21:00:00Z", + "modified": "2023-03-10T21:00:00Z", + "source_link": "https://github.com/Alexander-D-Karpov/cookiecutter-django", + "view_link": "" + } +}, +{ + "model": "about.project", + "pk": 23, + "fields": { + "name": "Exhauster analytics", + "description": "Ann app to process information from exhausters and try to warn and predict it", + "md": "", + "image": "uploads/images/photo_2023-02-19_07-33-56.jpg", + "created": "2023-02-18T21:00:00Z", + "modified": "2023-02-18T21:00:00Z", + "source_link": "https://github.com/evraz-hack/backend", + "view_link": "" + } +}, +{ + "model": "about.project", + "pk": 24, + "fields": { + "name": "Real estate search", + "description": "", + "md": "", + "image": "uploads/images/photo_2023-02-21_13-41-32.jpg", + "created": "2023-02-18T21:00:00Z", + "modified": "2023-02-18T21:00:00Z", + "source_link": "https://github.com/BlackWallTeam/Backend", + "view_link": "" + } +}, +{ + "model": "about.project", + "pk": 25, + "fields": { + "name": "Podcast poller script", + "description": "Script to load current listening track from yandex music and send it to telegram chat.", + "md": "", + "image": "uploads/images/photo_2023-02-21_13-43-35.jpg", + "created": "2023-02-19T21:00:00Z", + "modified": "2023-02-19T21:00:00Z", + "source_link": "https://github.com/Alexander-D-Karpov/scripts/tree/master/podcasts", + "view_link": "https://t.me/sanspie_podcasts" + } +} +] diff --git a/akarpov/templates/about/about.html b/akarpov/templates/about/about.html new file mode 100644 index 0000000..c607ab8 --- /dev/null +++ b/akarpov/templates/about/about.html @@ -0,0 +1,141 @@ +{% extends 'base.html' %} +{% block title %}sanspie: akarpov{% endblock title %} +{% block css %} + + + +{% endblock %} + +{% block content %} +
+
+
+

sanspie

+
+
+
Web developer
+
DevOps
+
CTF player
+
+
+
+ + + + + + + + + + +
+
+
+
+ My stack/technologies: +
(the higher, the better I know it)
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+
More about me:
+

I am 17 y. o. Python developer from Russia, specialized in creating Django web apps. I took part in the development of monolith websites on Django, Flask, FastAPI and microservices with Rest and Graphql. Also, I am interested in web app security and Linux servers administration.

+
+
+
+
+
+ +
+
+ + +
Projects:
+
+ {% for project in projects %} +
+ +
+
+
+ post image +
+
+
+
{{ project.name }}
+

{{ project.description|truncatewords:10 }}

+

+ {% if project.source_link %} + Source code {% endif %} {% if project.view_link %} + View project {% endif %} +

+

Created: {{ project.created|date:"d M Y" }}

+ {% if project.last_updated %} +

+ Last updated: {{ project.last_updated|date:"d M Y" }} +

+ {% endif %} +
+
+
+
+ +
+ {% endfor %} +
+{% endblock %} diff --git a/akarpov/templates/base.html b/akarpov/templates/base.html index eb7dad2..906ea4c 100644 --- a/akarpov/templates/base.html +++ b/akarpov/templates/base.html @@ -46,8 +46,8 @@ {% endif %}
  • - - Orders + + About me