diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..dfe0770
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,2 @@
+# Auto detect text files and perform LF normalization
+* text=auto
diff --git a/.gitignore b/.gitignore
index 55be276..582f2d0 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,3 @@
-# ---> Python
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
@@ -21,7 +20,6 @@ parts/
sdist/
var/
wheels/
-share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
@@ -40,17 +38,14 @@ pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
-.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
-*.py,cover
.hypothesis/
.pytest_cache/
-cover/
# Translations
*.mo
@@ -60,7 +55,6 @@ cover/
*.log
local_settings.py
db.sqlite3
-db.sqlite3-journal
# Flask stuff:
instance/
@@ -73,41 +67,16 @@ instance/
docs/_build/
# PyBuilder
-.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
-# IPython
-profile_default/
-ipython_config.py
-
# pyenv
-# For a library or package, you might want to ignore these files since the code is
-# intended to run in multiple environments; otherwise, check them in:
-# .python-version
+.python-version
-# pipenv
-# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
-# However, in case of collaboration, if having platform-specific dependencies or dependencies
-# having no cross-platform support, pipenv may install dependencies that don't work, or not
-# install all needed dependencies.
-#Pipfile.lock
-
-# poetry
-# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
-# This is especially recommended for binary packages to ensure reproducibility, and is more
-# commonly ignored for libraries.
-# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
-#poetry.lock
-
-# PEP 582; used by e.g. github.com/David-OConnor/pyflow
-__pypackages__/
-
-# Celery stuff
+# celery beat schedule file
celerybeat-schedule
-celerybeat.pid
# SageMath parsed files
*.sage.py
@@ -133,22 +102,9 @@ venv.bak/
# mypy
.mypy_cache/
-.dmypy.json
-dmypy.json
-# Pyre type checker
-.pyre/
-
-# pytype static type analyzer
-.pytype/
-
-# Cython debug symbols
-cython_debug/
-
-# PyCharm
-# JetBrains specific template is maintainted in a separate JetBrains.gitignore that can
-# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
-# and can be added to the global gitignore or merged into this file. For a more nuclear
-# option (not recommended) you can uncomment the following to ignore the entire idea folder.
-#.idea/
+# Jetbrains PyCharm
+.idea/
+# VSCode
+.vscode/
diff --git a/README.md b/README.md
deleted file mode 100644
index 2c2dba2..0000000
--- a/README.md
+++ /dev/null
@@ -1,2 +0,0 @@
-# jeweller-extend
-
diff --git a/README.rst b/README.rst
new file mode 100644
index 0000000..6849891
--- /dev/null
+++ b/README.rst
@@ -0,0 +1,15 @@
+=============
+jeweller-extend
+=============
+
+Small collection of reusabled components for Django Projects.
+
+
+Requirements:
+*************
+
+* Django==2.2.9
+* django-model-utils==4.0.0
+* django-mptt==0.11.0
+* easy-thumbnails==2.7
+* pytils==0.3
diff --git a/TODO.md b/TODO.md
new file mode 100644
index 0000000..0ede432
--- /dev/null
+++ b/TODO.md
@@ -0,0 +1,3 @@
+# TODO
+
+* Написать тесты
diff --git a/extend/__init__.py b/extend/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/extend/admin.py b/extend/admin.py
new file mode 100644
index 0000000..3c2c7d4
--- /dev/null
+++ b/extend/admin.py
@@ -0,0 +1,22 @@
+from django.contrib import admin
+from mptt.admin import MPTTModelAdmin
+
+
+class CategoryModelAdmin(MPTTModelAdmin):
+ list_display = ("id", "name", "slug")
+
+
+class TagModelAdmin(admin.ModelAdmin):
+ list_display = ("id", "name", "slug")
+
+
+def toggle_status_published(modeladmin, request, queryset):
+ queryset.update(status="published")
+
+
+def toggle_status_moderation(modeladmin, request, queryset):
+ queryset.update(status="moderation")
+
+
+toggle_status_published.short_description = "Опубликовать"
+toggle_status_moderation.short_description = "Отправить на модерацию"
diff --git a/extend/apps.py b/extend/apps.py
new file mode 100644
index 0000000..902f769
--- /dev/null
+++ b/extend/apps.py
@@ -0,0 +1,5 @@
+from django.apps import AppConfig
+
+
+class ExtendConfig(AppConfig):
+ name = "extend"
diff --git a/extend/migrations/__init__.py b/extend/migrations/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/extend/models.py b/extend/models.py
new file mode 100644
index 0000000..82aca54
--- /dev/null
+++ b/extend/models.py
@@ -0,0 +1,131 @@
+from datetime import datetime
+from django.db import models
+from django.contrib.auth.models import User
+from django.utils.safestring import mark_safe
+from mptt.models import MPTTModel, TreeForeignKey
+from pytils.translit import slugify
+from model_utils import Choices
+from easy_thumbnails.files import get_thumbnailer
+from easy_thumbnails.fields import ThumbnailerImageField
+
+
+class Category(MPTTModel):
+ name = models.CharField(
+ "Название", unique=True, max_length=100
+ )
+ slug = models.SlugField(
+ "Путь",
+ null=True,
+ blank=True,
+ unique=True,
+ max_length=100,
+ )
+ parent = TreeForeignKey(
+ "self",
+ on_delete=models.CASCADE,
+ null=True,
+ blank=True,
+ related_name="children",
+ )
+
+ class MPTTMeta:
+ order_insertion_by = ["name"]
+
+ class Meta:
+ abstract = True
+ verbose_name = "Категория"
+ verbose_name_plural = "Категории"
+ ordering = ["name"]
+
+ def __str__(self):
+ return self.name
+
+ def save(self, *args, **kwargs):
+ if not self.slug:
+ self.slug = slugify(self.name)
+ super().save(*args, **kwargs)
+
+
+class Tag(models.Model):
+ name = models.CharField(
+ "Название", unique=True, max_length=100
+ )
+ slug = models.SlugField(
+ "Путь",
+ null=True,
+ blank=True,
+ unique=True,
+ max_length=100,
+ )
+
+ class Meta:
+ abstract = True
+ verbose_name = "Метка"
+ verbose_name_plural = "Метки"
+ ordering = ["name"]
+
+ def __str__(self):
+ return self.name
+
+ def save(self, *args, **kwargs):
+ if not self.slug:
+ self.slug = slugify(self.name)
+ super().save(*args, **kwargs)
+
+
+class Photo(models.Model):
+ user = models.ForeignKey(User, null=True, on_delete=models.SET_NULL, verbose_name="Пользователь")
+ title = models.CharField("Название", blank=True, max_length=255)
+ image = ThumbnailerImageField(
+ "Фото", upload_to="photos/", max_length=255, default="", resize_source=dict(size=(825, 825), sharpen=True)
+ )
+ created = models.DateTimeField("Создано", default=datetime.now)
+ updated = models.DateTimeField("Изменено", auto_now=True)
+
+ class Meta:
+ abstract = True
+ verbose_name = "Фотография"
+ verbose_name_plural = "Фотографии"
+ ordering = ["id"]
+
+ def __str__(self):
+ return self.title
+
+ def image_tag(self):
+ if self.image:
+ options = {'size': (100, 100), 'upscale': True}
+ thumb_url = get_thumbnailer(self.image).get_thumbnail(options).url
+ return mark_safe('
' % thumb_url)
+ else:
+ return 'Изображение отсутствует'
+
+ image_tag.short_description = 'Превью'
+
+
+class Comment(models.Model):
+ user = models.ForeignKey(User, null=True, on_delete=models.CASCADE)
+ text = models.TextField("Текст")
+ created = models.DateTimeField(
+ "Создано", auto_now_add=True
+ )
+ updated = models.DateTimeField(
+ "Изменено", auto_now=True
+ )
+ STATUS = Choices(
+ ("moderation", "На модерации"),
+ ("published", "Опубликовано"),
+ )
+ status = models.CharField(
+ choices=STATUS,
+ default=STATUS.moderation,
+ max_length=20,
+ )
+
+ class Meta:
+ abstract = True
+ verbose_name = "Комментарий"
+ verbose_name_plural = "Комментарии"
+ ordering = ["-created"]
+
+ def __str__(self):
+ return f'{self.created} {self.user.username}'
diff --git a/extend/tests.py b/extend/tests.py
new file mode 100644
index 0000000..e69de29
diff --git a/extend/utils.py b/extend/utils.py
new file mode 100644
index 0000000..bb92d21
--- /dev/null
+++ b/extend/utils.py
@@ -0,0 +1,25 @@
+import re
+from django.core.exceptions import ValidationError
+from django.utils.crypto import get_random_string
+from django.utils.translation import gettext_lazy as _
+
+
+def validate_url(value):
+ """
+ Валидация ссылок на видео с видеохостинга YouTube
+ """
+ video_url = value
+ regex = re.compile(
+ r"(https?://)?(www\.)?(youtube|youtu|youtube-nocookie)\.(com|be)/(watch\?v=|embed/|v/|.+\?v=)?(?P[A-Za-z0-9\-=_]{11})"
+ )
+ match = regex.match(video_url)
+ if not match:
+ raise ValidationError(
+ _("%(value)s не является корректной YouTube ссылкой"),
+ params={"value": value},
+ )
+
+
+def generate_secret_key():
+ chars = "abcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*(-_=+)"
+ return get_random_string(50, chars)
diff --git a/extend/views.py b/extend/views.py
new file mode 100644
index 0000000..e69de29
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000..046b243
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,5 @@
+Django==2.2.9
+django-model-utils==4.0.0
+django-mptt==0.11.0
+easy-thumbnails==2.7
+pytils==0.3
diff --git a/setup.py b/setup.py
new file mode 100644
index 0000000..221f42e
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,37 @@
+import os
+from setuptools import find_packages, setup
+
+with open(os.path.join(os.path.dirname(__file__), 'README.rst')) as readme:
+ README = readme.read()
+
+# allow setup.py to be run from any path
+os.chdir(os.path.normpath(os.path.join(os.path.abspath(__file__), os.pardir)))
+
+setup(
+ name='jeweller-extend',
+ version='0.2.1',
+ package_dir={'extend': 'extend'},
+ packages=find_packages(),
+ include_package_data=True,
+ license='BSD License',
+ description='Small collection of reusabled components for Django.',
+ long_description=README,
+ url='https://github.com/stargot/jeweller-extend/',
+ author='Ivan Sinyavskiy',
+ author_email='stargot@gmail.com',
+ classifiers=[
+ 'Environment :: Web Environment',
+ 'Framework :: Django',
+ 'Framework :: Django :: 2.2',
+ 'Framework :: Django :: 3.0',
+ 'Intended Audience :: Developers',
+ 'License :: OSI Approved :: BSD License',
+ 'Operating System :: OS Independent',
+ 'Programming Language :: Python',
+ 'Programming Language :: Python :: 3.6',
+ 'Programming Language :: Python :: 3.7',
+ 'Programming Language :: Python :: 3.8',
+ 'Topic :: Internet :: WWW/HTTP',
+ 'Topic :: Internet :: WWW/HTTP :: Dynamic Content',
+ ],
+)