diff --git a/avatar/api/__init__.py b/avatar/api/__init__.py
new file mode 100644
index 0000000..d087506
--- /dev/null
+++ b/avatar/api/__init__.py
@@ -0,0 +1 @@
+import avatar.api.signals
diff --git a/avatar/api/apps.py b/avatar/api/apps.py
new file mode 100644
index 0000000..f139ac4
--- /dev/null
+++ b/avatar/api/apps.py
@@ -0,0 +1,6 @@
+from django.apps import AppConfig
+
+
+class ApiConfig(AppConfig):
+ default_auto_field = "django.db.models.BigAutoField"
+ name = "avatar.api"
diff --git a/avatar/api/conf.py b/avatar/api/conf.py
new file mode 100644
index 0000000..8e601c5
--- /dev/null
+++ b/avatar/api/conf.py
@@ -0,0 +1,7 @@
+from appconf import AppConf
+from django.conf import settings
+
+
+class AvatarAPIConf(AppConf):
+ # allow updating avatar image in put method
+ AVATAR_CHANGE_IMAGE = False
diff --git a/avatar/api/docs/Makefile b/avatar/api/docs/Makefile
new file mode 100644
index 0000000..d4bb2cb
--- /dev/null
+++ b/avatar/api/docs/Makefile
@@ -0,0 +1,20 @@
+# Minimal makefile for Sphinx documentation
+#
+
+# You can set these variables from the command line, and also
+# from the environment for the first two.
+SPHINXOPTS ?=
+SPHINXBUILD ?= sphinx-build
+SOURCEDIR = .
+BUILDDIR = _build
+
+# Put it first so that "make" without argument is like "make help".
+help:
+ @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
+
+.PHONY: help Makefile
+
+# Catch-all target: route all unknown targets to Sphinx using the new
+# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
+%: Makefile
+ @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
diff --git a/avatar/api/docs/avatar.rst b/avatar/api/docs/avatar.rst
new file mode 100644
index 0000000..f311ee9
--- /dev/null
+++ b/avatar/api/docs/avatar.rst
@@ -0,0 +1,191 @@
+
+API Descriptions
+================
+
+Avatar List
+^^^^^^^^^^^
+
+
+send a request for listing user avatars as shown below.
+
+``GET`` ``/api/avatar/``
+
+
+
+ default response of avatar list : ::
+
+ {
+ "message": "You haven't uploaded an avatar yet. Please upload one now.",
+ "default_avatar": {
+ "src": "https://seccdn.libravatar.org/avatar/4a9328d595472d0728195a7c8191a50b",
+ "width": "80",
+ "height": "80",
+ "alt": "User Avatar"
+ }
+ }
+
+
+ if you have an avatar object : ::
+
+ [
+ {
+ "id": "image_id",
+ "avatar_url": "https://example.com/api/avatar/1/",
+ "avatar": "https://example.com/media/avatars/1/first_avatar.png",
+ "primary": true
+ },
+ ]
+
+
+
+-----------------------------------------------
+
+Create Avatar
+^^^^^^^^^^^^^
+
+
+send a request for creating user avatar as shown below .
+
+``POST`` ``/api/avatar/``
+
+
+ Request : ::
+
+ {
+ "avatar": "image file",
+ "primary": true
+ }
+
+ ``Note`` : avatar field is required.
+
+ Response : ::
+
+ {
+ "message": "Successfully uploaded a new avatar.",
+ "data": {
+ "id": "image_id",
+ "avatar_url": "https://example.com/api/avatar/1/",
+ "avatar": "https://example.com/media/avatars/1/example.png",
+ "primary": true
+ }
+ }
+
+
+
+-----------------------------------------------
+
+Avatar Detail
+^^^^^^^^^^^^^
+
+
+send a request for retrieving user avatar.
+
+``GET`` ``/api/avatar/image_id/``
+
+
+ Response : ::
+
+ {
+ "id": "image_id",
+ "avatar": "https://example.com/media/avatars/1/example.png",
+ "primary": true
+ }
+
+
+
+-----------------------------------------------
+
+Update Avatar
+^^^^^^^^^^^^^
+
+
+send a request for updating user avatar.
+
+``PUT`` ``/api/avatar/image_id/``
+
+
+ Request : ::
+
+ {
+ "avatar":"image file"
+ "primary": true
+ }
+
+ ``Note`` : for update avatar image set ``API_AVATAR_CHANGE_IMAGE = True`` in your settings file and set ``primary = True``.
+
+ Response : ::
+
+ {
+ "message": "Successfully updated your avatar.",
+ "data": {
+ "id": "image_id",
+ "avatar": "https://example.com/media/avatars/1/custom_admin_en.png",
+ "primary": true
+ }
+ }
+
+-----------------------------------------------
+
+Delete Avatar
+^^^^^^^^^^^^^
+
+
+send a request for deleting user avatar.
+
+``DELETE`` ``/api/avatar/image_id/``
+
+
+ Response : ::
+
+ "Successfully deleted the requested avatars."
+
+
+
+
+-----------------------------------------------
+
+Render Primary Avatar
+^^^^^^^^^^^^^^^^^^^^^
+
+send a request for retrieving resized primary avatar .
+
+
+default sizes ``80``:
+
+``GET`` ``/api/avatar/render_primary/``
+
+ Response : ::
+
+ {
+ "image_url": "https://example.com/media/avatars/1/resized/80/80/example.png"
+ }
+
+custom ``width`` and ``height`` :
+
+``GET`` ``/api/avatar/render_primary/?width=width_size&height=height_size``
+
+ Response : ::
+
+ {
+ "image_url": "http://127.0.0.1:8000/media/avatars/1/resized/width_size/height_size/python.png"
+ }
+
+
+If the entered parameter is one of ``width`` or ``height``, it will be considered for both .
+
+``GET`` ``/api/avatar/render_primary/?width=size`` :
+
+ Response : ::
+
+ {
+ "image_url": "http://127.0.0.1:8000/media/avatars/1/resized/size/size/python.png"
+ }
+
+``Note`` : Resize parameters not working for default avatar.
+
+API Setting
+===========
+
+.. py:data:: API_AVATAR_CHANGE_IMAGE
+
+ It Allows the user to Change the avatar image in ``PUT`` method. Default is ``False``.
diff --git a/avatar/api/docs/conf.py b/avatar/api/docs/conf.py
new file mode 100644
index 0000000..743b82a
--- /dev/null
+++ b/avatar/api/docs/conf.py
@@ -0,0 +1,33 @@
+# Configuration file for the Sphinx documentation builder.
+#
+# For the full list of built-in configuration values, see the documentation:
+# https://www.sphinx-doc.org/en/master/usage/configuration.html
+
+# -- Project information -----------------------------------------------------
+# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
+
+project = "django-avatar"
+copyright = "2013, django-avatar developers"
+author = "keyvan"
+
+# -- General configuration ---------------------------------------------------
+# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
+
+extensions = []
+
+templates_path = ["_templates"]
+exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]
+
+htmlhelp_basename = "django-avatar apidoc"
+
+source_suffix = ".rst"
+
+master_doc = "index"
+pygments_style = "sphinx"
+
+# -- Options for HTML output -------------------------------------------------
+# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output
+
+html_theme = "sphinx_rtd_theme"
+html_static_path = ["_static"]
+epub_show_urls = "footnote"
diff --git a/avatar/api/docs/index.rst b/avatar/api/docs/index.rst
new file mode 100644
index 0000000..e28688c
--- /dev/null
+++ b/avatar/api/docs/index.rst
@@ -0,0 +1,310 @@
+
+django-avatar
+=============
+
+Django-avatar is a reusable application for handling user avatars. It has the
+ability to default to avatars provided by third party services (like Gravatar_
+or Facebook) if no avatar is found for a certain user. Django-avatar
+automatically generates thumbnails and stores them to your default file
+storage backend for retrieval later.
+
+.. _Gravatar: https://gravatar.com
+
+Installation
+------------
+
+If you have pip_ installed, you can simply run the following command
+to install django-avatar::
+
+ pip install django-avatar
+
+Included with this application is a file named ``setup.py``. It's possible to
+use this file to install this application to your system, by invoking the
+following command::
+
+ python setup.py install
+
+Once that's done, you should be able to begin using django-avatar at will.
+
+Usage
+-----
+
+To integrate ``django-avatar`` with your site, there are relatively few things
+that are required. A minimal integration can work like this:
+
+1. List this application in the ``INSTALLED_APPS`` portion of your settings
+ file. Your settings file will look something like::
+
+ INSTALLED_APPS = (
+ # ...
+ 'avatar',
+ )
+
+2. Migrate your database::
+
+ python manage.py migrate
+
+3. Add the avatar urls to the end of your root urlconf. Your urlconf
+ will look something like::
+
+ urlpatterns = [
+ # ...
+ path('avatar/', include('avatar.urls')),
+ ]
+
+4. Somewhere in your template navigation scheme, link to the change avatar
+ page::
+
+ Change your avatar
+
+5. Wherever you want to display an avatar for a user, first load the avatar
+ template tags::
+
+ {% load avatar_tags %}
+
+ Then, use the ``avatar`` tag to display an avatar of a default size::
+
+ {% avatar user %}
+
+ Or specify a size (in pixels) explicitly::
+
+ {% avatar user 65 %}
+
+ Or specify a width and height (in pixels) explicitly::
+
+ {% avatar user 65 50 %}
+
+ Example for customize the attribute of the HTML ``img`` tag::
+
+ {% avatar user 65 class="img-circle img-responsive" id="user_avatar" %}
+
+Template tags and filter
+------------------------
+
+To begin using these template tags, you must first load the tags into the
+template rendering system::
+
+ {% load avatar_tags %}
+
+``{% avatar_url user [size in pixels] %}``
+ Renders the URL of the avatar for the given user. User can be either a
+ ``django.contrib.auth.models.User`` object instance or a username.
+
+``{% avatar user [size in pixels] **kwargs %}``
+ Renders an HTML ``img`` tag for the given user for the specified size. User
+ can be either a ``django.contrib.auth.models.User`` object instance or a
+ username. The (key, value) pairs in kwargs will be added to ``img`` tag
+ as its attributes.
+
+``{% render_avatar avatar [size in pixels] %}``
+ Given an actual ``avatar.models.Avatar`` object instance, renders an HTML
+ ``img`` tag to represent that avatar at the requested size.
+
+``{{ request.user|has_avatar }}``
+ Given a user object returns a boolean if the user has an avatar.
+
+Global Settings
+---------------
+
+There are a number of settings available to easily customize the avatars that
+appear on the site. Listed below are those settings:
+
+.. py:data:: AVATAR_AUTO_GENERATE_SIZES
+
+ An iterable of integers and/or sequences in the format ``(width, height)``
+ representing the sizes of avatars to generate on upload. This can save
+ rendering time later on if you pre-generate the resized versions. Defaults
+ to ``(80,)``.
+
+.. py:data:: AVATAR_CACHE_ENABLED
+
+ Set to ``False`` if you completely disable avatar caching. Defaults to ``True``.
+
+.. py:data:: AVATAR_DEFAULT_URL
+
+ The default URL to default to if the
+ :py:class:`~avatar.providers.GravatarAvatarProvider` is not used and there
+ is no ``Avatar`` instance found in the system for the given user.
+
+.. py:data:: AVATAR_EXPOSE_USERNAMES
+
+ Puts the User's username field in the URL path when ``True``. Set to
+ ``False`` to use the User's primary key instead, preventing their email
+ from being searchable on the web. Defaults to ``False``.
+
+.. py:data:: AVATAR_FACEBOOK_GET_ID
+
+ A callable or string path to a callable that will return the user's
+ Facebook ID. The callable should take a ``User`` object and return a
+ string. If you want to use this then make sure you included
+ :py:class:`~avatar.providers.FacebookAvatarProvider` in :py:data:`AVATAR_PROVIDERS`.
+
+.. py:data:: AVATAR_GRAVATAR_DEFAULT
+
+ A string determining the default Gravatar. Can be a URL to a custom image
+ or a style of Gravatar. Ex. `retro`. All Available options listed in the
+ `Gravatar documentation `_. Defaults to ``None``.
+
+.. py:data:: AVATAR_GRAVATAR_FORCEDEFAULT
+
+ A bool indicating whether or not to always use the default Gravitar. More
+ details can be found in the `Gravatar documentation
+ `_. Defaults
+ to ``False``.
+
+.. py:data:: AVATAR_GRAVATAR_FIELD
+
+ The name of the user's field containing the gravatar email. For example,
+ if you set this to ``gravatar`` then django-avatar will get the user's
+ gravatar in ``user.gravatar``. Defaults to ``email``.
+
+.. py:data:: AVATAR_MAX_SIZE
+
+ File size limit for avatar upload. Default is ``1024 * 1024`` (1 MB).
+ gravatar in ``user.gravatar``.
+
+.. py:data:: AVATAR_MAX_AVATARS_PER_USER
+
+ The maximum number of avatars each user can have. Default is ``42``.
+
+.. py:data:: AVATAR_PATH_HANDLER
+
+ Path to a method for avatar file path handling. Default is
+ ``avatar.models.avatar_path_handler``.
+
+.. py:data:: AVATAR_PROVIDERS
+
+ Tuple of classes that are tried in the given order for returning avatar
+ URLs.
+ Defaults to::
+
+ (
+ 'avatar.providers.PrimaryAvatarProvider',
+ 'avatar.providers.LibRAvatarProvider',
+ 'avatar.providers.GravatarAvatarProvider',
+ 'avatar.providers.DefaultAvatarProvider',
+ )
+
+ If you want to implement your own provider, it must provide a class method
+ ``get_avatar_url(user, width, height)``.
+
+ .. py:class:: avatar.providers.PrimaryAvatarProvider
+
+ Returns the primary avatar stored for the given user.
+
+ .. py:class:: avatar.providers.GravatarAvatarProvider
+
+ Adds support for the Gravatar service and will always return an avatar
+ URL. If the user has no avatar registered with Gravatar a default will
+ be used (see :py:data:`AVATAR_GRAVATAR_DEFAULT`).
+
+ .. py:class:: avatar.providers.FacebookAvatarProvider
+
+ Add this provider to :py:data:`AVATAR_PROVIDERS` in order to add
+ support for profile images from Facebook. Note that you also need to
+ set the :py:data:`AVATAR_FACEBOOK_GET_ID` setting.
+
+ .. py:class:: avatar.providers.DefaultAvatarProvider
+
+ Provides a fallback avatar defined in :py:data:`AVATAR_DEFAULT_URL`.
+
+.. py:data:: AVATAR_RESIZE_METHOD
+
+ The method to use when resizing images, based on the options available in
+ Pillow. Defaults to ``Image.LANCZOS``.
+
+.. py:data:: AVATAR_STORAGE_DIR
+
+ The directory under ``MEDIA_ROOT`` to store the images. If using a
+ non-filesystem storage device, this will simply be appended to the beginning
+ of the file name. Defaults to ``avatars``.
+ Pillow. Defaults to ``Image.ANTIALIAS``.
+
+.. py:data:: AVATAR_THUMB_FORMAT
+
+ The file format of thumbnails, based on the options available in
+ Pillow. Defaults to `PNG`.
+
+.. py:data:: AVATAR_THUMB_QUALITY
+
+ The quality of thumbnails, between 0 (worst) to 95 (best) or the string
+ "keep" (only JPEG) as provided by Pillow. Defaults to `85`.
+
+.. py:data:: AVATAR_THUMB_MODES
+
+ A sequence of acceptable modes for thumbnails as provided by Pillow. If the mode
+ of the image is not in the list, the thumbnail will be converted to the
+ first mode in the list. Defaults to `('RGB', 'RGBA')`.
+
+.. py:data:: AVATAR_CLEANUP_DELETED
+
+ ``True`` if the avatar image files should be deleted when an avatar is
+ deleted from the database. Defaults to ``True``.
+
+.. py:data:: AVATAR_ADD_TEMPLATE
+
+ Path to the Django template to use for adding a new avatar. Defaults to
+ ``avatar/add.html``.
+
+.. py:data:: AVATAR_CHANGE_TEMPLATE
+
+ Path to the Django template to use for changing a user's avatar. Defaults to ``avatar/change.html``.
+
+.. py:data:: AVATAR_DELETE_TEMPLATE
+
+ Path to the Django template to use for confirming a delete of a user's
+ avatar. Defaults to ``avatar/avatar/confirm_delete.html``.
+
+.. py:data:: AVATAR_ALLOWED_MIMETYPES
+
+ Limit allowed avatar image uploads by their actual content payload and what image codecs we wish to support.
+ This limits website user content site attack vectors against image codec buffer overflow and similar bugs.
+ `You must have python-imaging library installed `_.
+ Suggested safe setting: ``("image/png", "image/gif", "image/jpeg")``.
+ When enabled you'll get the following error on the form upload *File content is invalid. Detected: image/tiff Allowed content types are: image/png, image/gif, image/jpg*.
+
+
+Management Commands
+-------------------
+
+This application does include one management command: ``rebuild_avatars``. It
+takes no arguments and, when run, re-renders all of the thumbnails for all of
+the avatars for the pixel sizes specified in the
+:py:data:`AVATAR_AUTO_GENERATE_SIZES` setting.
+
+
+.. _pip: https://www.pip-installer.org/
+
+-----------------------------------------------
+
+
+API
+---
+
+To use API there are relatively few things that are required.
+
+after `Installation <#installation>`_ .
+
+1. in your ``INSTALLED_APPS`` of your settings file : ::
+
+ INSTALLED_APPS = (
+ # ...
+ 'avatar',
+ 'rest_framework'
+ )
+
+
+2. Add the avatar api urls to the end of your root url config : ::
+
+ urlpatterns = [
+ # ...
+ path('api/', include('avatar.api.urls')),
+ ]
+
+-----------------------------------------------
+
+.. toctree::
+ :maxdepth: 1
+
+ avatar
diff --git a/avatar/api/docs/make.bat b/avatar/api/docs/make.bat
new file mode 100644
index 0000000..954237b
--- /dev/null
+++ b/avatar/api/docs/make.bat
@@ -0,0 +1,35 @@
+@ECHO OFF
+
+pushd %~dp0
+
+REM Command file for Sphinx documentation
+
+if "%SPHINXBUILD%" == "" (
+ set SPHINXBUILD=sphinx-build
+)
+set SOURCEDIR=.
+set BUILDDIR=_build
+
+%SPHINXBUILD% >NUL 2>NUL
+if errorlevel 9009 (
+ echo.
+ echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
+ echo.installed, then set the SPHINXBUILD environment variable to point
+ echo.to the full path of the 'sphinx-build' executable. Alternatively you
+ echo.may add the Sphinx directory to PATH.
+ echo.
+ echo.If you don't have Sphinx installed, grab it from
+ echo.https://www.sphinx-doc.org/
+ exit /b 1
+)
+
+if "%1" == "" goto help
+
+%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
+goto end
+
+:help
+%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
+
+:end
+popd
diff --git a/avatar/api/migrations/__init__.py b/avatar/api/migrations/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/avatar/api/requirements.txt b/avatar/api/requirements.txt
new file mode 100644
index 0000000..6f1c1f3
--- /dev/null
+++ b/avatar/api/requirements.txt
@@ -0,0 +1 @@
+djangorestframework
diff --git a/avatar/api/serializers.py b/avatar/api/serializers.py
new file mode 100644
index 0000000..6dc1b62
--- /dev/null
+++ b/avatar/api/serializers.py
@@ -0,0 +1,126 @@
+import os
+
+from django.template.defaultfilters import filesizeformat
+from django.utils.translation import gettext_lazy as _
+from PIL import Image, ImageOps
+from rest_framework import serializers
+
+from avatar.conf import settings
+from avatar.conf import settings as api_setting
+from avatar.models import Avatar
+
+
+class AvatarSerializer(serializers.ModelSerializer):
+ avatar_url = serializers.HyperlinkedIdentityField(
+ view_name="avatar-detail",
+ )
+ user = serializers.HiddenField(default=serializers.CurrentUserDefault())
+
+ class Meta:
+ model = Avatar
+ fields = ["id", "avatar_url", "avatar", "primary", "user"]
+ extra_kwargs = {"avatar": {"required": True}}
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ request = kwargs.get("context").get("request", None)
+
+ self.user = request.user
+
+ def get_fields(self, *args, **kwargs):
+ fields = super(AvatarSerializer, self).get_fields(*args, **kwargs)
+ request = self.context.get("request", None)
+
+ # remove avatar url field in detail page
+ if bool(self.context.get("view").kwargs):
+ fields.pop("avatar_url")
+
+ # remove avatar field in put method
+ if request and getattr(request, "method", None) == "PUT":
+ # avatar updates only when primary=true and API_AVATAR_CHANGE_IMAGE = True
+ if (
+ not api_setting.API_AVATAR_CHANGE_IMAGE
+ or self.instance
+ and not self.instance.primary
+ ):
+ fields.pop("avatar")
+ else:
+ fields.get("avatar", None).required = False
+ return fields
+
+ def validate_avatar(self, value):
+ data = value
+
+ if settings.AVATAR_ALLOWED_MIMETYPES:
+ try:
+ import magic
+ except ImportError:
+ raise ImportError(
+ "python-magic library must be installed in order to use uploaded file content limitation"
+ )
+
+ # Construct 256 bytes needed for mime validation
+ magic_buffer = bytes()
+ for chunk in data.chunks():
+ magic_buffer += chunk
+ if len(magic_buffer) >= 256:
+ break
+
+ # https://github.com/ahupp/python-magic#usage
+ mime = magic.from_buffer(magic_buffer, mime=True)
+ if mime not in settings.AVATAR_ALLOWED_MIMETYPES:
+ raise serializers.ValidationError(
+ _(
+ "File content is invalid. Detected: %(mimetype)s Allowed content types are: %(valid_mime_list)s"
+ )
+ % {
+ "valid_mime_list": ", ".join(settings.AVATAR_ALLOWED_MIMETYPES),
+ "mimetype": mime,
+ }
+ )
+
+ if settings.AVATAR_ALLOWED_FILE_EXTS:
+ root, ext = os.path.splitext(data.name.lower())
+ if ext not in settings.AVATAR_ALLOWED_FILE_EXTS:
+ valid_exts = ", ".join(settings.AVATAR_ALLOWED_FILE_EXTS)
+ error = _(
+ "%(ext)s is an invalid file extension. "
+ "Authorized extensions are : %(valid_exts_list)s"
+ )
+ raise serializers.ValidationError(
+ error % {"ext": ext, "valid_exts_list": valid_exts}
+ )
+
+ if data.size > settings.AVATAR_MAX_SIZE:
+ error = _(
+ "Your file is too big (%(size)s), "
+ "the maximum allowed size is %(max_valid_size)s"
+ )
+ raise serializers.ValidationError(
+ error
+ % {
+ "size": filesizeformat(data.size),
+ "max_valid_size": filesizeformat(settings.AVATAR_MAX_SIZE),
+ }
+ )
+
+ try:
+ image = Image.open(data)
+ ImageOps.exif_transpose(image)
+ except TypeError:
+ raise serializers.ValidationError(_("Corrupted image"))
+
+ count = Avatar.objects.filter(user=self.user).count()
+ if 1 < settings.AVATAR_MAX_AVATARS_PER_USER <= count:
+ error = _(
+ "You already have %(nb_avatars)d avatars, "
+ "and the maximum allowed is %(nb_max_avatars)d."
+ )
+ raise serializers.ValidationError(
+ error
+ % {
+ "nb_avatars": count,
+ "nb_max_avatars": settings.AVATAR_MAX_AVATARS_PER_USER,
+ }
+ )
+ return data
diff --git a/avatar/api/shortcut.py b/avatar/api/shortcut.py
new file mode 100644
index 0000000..a5b5b62
--- /dev/null
+++ b/avatar/api/shortcut.py
@@ -0,0 +1,27 @@
+from django.shortcuts import _get_queryset
+
+
+def get_object_or_none(klass, *args, **kwargs):
+ """
+ Use get() to return an object, or return None if the object
+ does not exist.
+
+ klass may be a Model, Manager, or QuerySet object. All other passed
+ arguments and keyword arguments are used in the get() query.
+
+ Like with QuerySet.get(), MultipleObjectsReturned is raised if more than
+ one object is found.
+ """
+ queryset = _get_queryset(klass)
+ if not hasattr(queryset, "get"):
+ klass__name = (
+ klass.__name__ if isinstance(klass, type) else klass.__class__.__name__
+ )
+ raise ValueError(
+ "First argument to get_object_or_404() must be a Model, Manager, "
+ "or QuerySet, not '%s'." % klass__name
+ )
+ try:
+ return queryset.get(*args, **kwargs)
+ except queryset.model.DoesNotExist:
+ return None
diff --git a/avatar/api/signals.py b/avatar/api/signals.py
new file mode 100644
index 0000000..14c5dbf
--- /dev/null
+++ b/avatar/api/signals.py
@@ -0,0 +1,56 @@
+import os
+
+from django.db.models import signals
+
+from avatar.api.conf import settings as api_settings
+from avatar.api.shortcut import get_object_or_none
+from avatar.conf import settings
+from avatar.models import Avatar, invalidate_avatar_cache
+
+
+def create_default_thumbnails(sender, instance, created=False, **kwargs):
+ invalidate_avatar_cache(sender, instance)
+
+ if not created:
+ for size in settings.AVATAR_AUTO_GENERATE_SIZES:
+ if isinstance(size, int):
+ if not instance.thumbnail_exists(size, size):
+ instance.create_thumbnail(size, size)
+ else:
+ # Size is specified with height and width.
+ if not instance.thumbnail_exists(size[0, size[0]]):
+ instance.create_thumbnail(size[0], size[1])
+
+
+def remove_previous_avatar_images_when_update(
+ sender, instance=None, created=False, update_main_avatar=True, **kwargs
+):
+ if not created:
+ old_instance = get_object_or_none(Avatar, pk=instance.pk)
+ if old_instance and not old_instance.avatar == instance.avatar:
+ base_filepath = old_instance.avatar.name
+ path, filename = os.path.split(base_filepath)
+ # iterate through resized avatars directories and delete resized avatars
+ resized_path = os.path.join(path, "resized")
+ try:
+ resized_widths, _ = old_instance.avatar.storage.listdir(resized_path)
+ for width in resized_widths:
+ resized_width_path = os.path.join(resized_path, width)
+ resized_heights, _ = old_instance.avatar.storage.listdir(
+ resized_width_path
+ )
+ for height in resized_heights:
+ if old_instance.thumbnail_exists(width, height):
+ old_instance.avatar.storage.delete(
+ old_instance.avatar_name(width, height)
+ )
+ if update_main_avatar:
+ if old_instance.avatar.storage.exists(old_instance.avatar.name):
+ old_instance.avatar.storage.delete(old_instance.avatar.name)
+ except FileNotFoundError as e:
+ pass
+
+
+if api_settings.API_AVATAR_CHANGE_IMAGE:
+ signals.pre_save.connect(remove_previous_avatar_images_when_update, sender=Avatar)
+ signals.post_save.connect(create_default_thumbnails, sender=Avatar)
diff --git a/avatar/api/urls.py b/avatar/api/urls.py
new file mode 100644
index 0000000..91dce0d
--- /dev/null
+++ b/avatar/api/urls.py
@@ -0,0 +1,8 @@
+from rest_framework.routers import SimpleRouter
+
+from avatar.api.views import AvatarViewSets
+
+router = SimpleRouter()
+router.register("avatar", AvatarViewSets)
+
+urlpatterns = router.urls
diff --git a/avatar/api/utils.py b/avatar/api/utils.py
new file mode 100644
index 0000000..9784fc6
--- /dev/null
+++ b/avatar/api/utils.py
@@ -0,0 +1,52 @@
+from html.parser import HTMLParser
+
+from avatar.conf import settings
+
+
+class HTMLTagParser(HTMLParser):
+ """
+ URL parser for getting (url ,width ,height) from avatar templatetags
+ """
+
+ def __init__(self, output=None):
+ HTMLParser.__init__(self)
+ if output is None:
+ self.output = {}
+ else:
+ self.output = output
+
+ def handle_starttag(self, tag, attrs):
+ self.output.update(dict(attrs))
+
+
+def assign_width_or_height(query_params):
+ """
+ Getting width and height in url parameters and specifying them
+ """
+ avatar_default_size = settings.AVATAR_DEFAULT_SIZE
+
+ width = query_params.get("width", avatar_default_size)
+ height = query_params.get("height", avatar_default_size)
+
+ if width == "":
+ width = avatar_default_size
+ if height == "":
+ height = avatar_default_size
+
+ if height == avatar_default_size and height != "":
+ height = width
+ elif width == avatar_default_size and width != "":
+ width = height
+
+ width = int(width)
+ height = int(height)
+
+ context = {"width": width, "height": height}
+ return context
+
+
+def set_new_primary(query_set, instance):
+ queryset = query_set.exclude(id=instance.id).first()
+ if queryset:
+ queryset.primary = True
+ queryset.save()
diff --git a/avatar/api/views.py b/avatar/api/views.py
new file mode 100644
index 0000000..7b41a4f
--- /dev/null
+++ b/avatar/api/views.py
@@ -0,0 +1,134 @@
+from django.db.models import QuerySet
+from django.utils.translation import gettext_lazy as _
+from rest_framework import permissions, status, viewsets
+from rest_framework.decorators import action
+from rest_framework.exceptions import ValidationError
+from rest_framework.response import Response
+
+from avatar.api.serializers import AvatarSerializer
+from avatar.api.utils import HTMLTagParser, assign_width_or_height, set_new_primary
+from avatar.models import Avatar
+from avatar.templatetags.avatar_tags import avatar
+from avatar.utils import get_default_avatar_url, get_primary_avatar, invalidate_cache
+
+
+class AvatarViewSets(viewsets.ModelViewSet):
+ serializer_class = AvatarSerializer
+ permission_classes = [permissions.IsAuthenticated]
+ queryset = Avatar.objects.select_related("user").order_by(
+ "-primary", "-date_uploaded"
+ )
+
+ @property
+ def parse_html_to_json(self):
+ default_avatar = avatar(self.request.user)
+ html_parser = HTMLTagParser()
+ html_parser.feed(default_avatar)
+ return html_parser.output
+
+ def get_queryset(self):
+ assert self.queryset is not None, (
+ "'%s' should either include a `queryset` attribute, "
+ "or override the `get_queryset()` method." % self.__class__.__name__
+ )
+
+ queryset = self.queryset
+ if isinstance(queryset, QuerySet):
+ # Ensure queryset is re-evaluated on each request.
+ queryset = queryset.filter(user=self.request.user)
+ return queryset
+
+ def list(self, request, *args, **kwargs):
+ queryset = self.filter_queryset(self.get_queryset())
+ if queryset:
+ page = self.paginate_queryset(queryset)
+ if page is not None:
+ serializer = self.get_serializer(page, many=True)
+ return self.get_paginated_response(serializer.data)
+
+ serializer = self.get_serializer(queryset, many=True)
+ data = serializer.data
+ return Response(data)
+
+ return Response(
+ {
+ "message": "You haven't uploaded an avatar yet. Please upload one now.",
+ "default_avatar": self.parse_html_to_json,
+ }
+ )
+
+ def create(self, request, *args, **kwargs):
+ serializer = self.get_serializer(data=request.data)
+ serializer.is_valid(raise_exception=True)
+ self.perform_create(serializer)
+ headers = self.get_success_headers(serializer.data)
+ message = _("Successfully uploaded a new avatar.")
+
+ context_data = {"message": message, "data": serializer.data}
+ return Response(context_data, status=status.HTTP_201_CREATED, headers=headers)
+
+ def destroy(self, request, *args, **kwargs):
+ instance = self.get_object()
+ if instance.primary is True:
+ # Find the next avatar, and set it as the new primary
+ set_new_primary(self.get_queryset(), instance)
+ self.perform_destroy(instance)
+ message = _("Successfully deleted the requested avatars.")
+ return Response(message, status=status.HTTP_204_NO_CONTENT)
+
+ def update(self, request, *args, **kwargs):
+ partial = kwargs.pop("partial", False)
+ instance = self.get_object()
+ serializer = self.get_serializer(instance, data=request.data, partial=partial)
+ serializer.is_valid(raise_exception=True)
+ avatar_image = serializer.validated_data.get("avatar")
+ primary_avatar = serializer.validated_data.get("primary")
+ if not primary_avatar and avatar_image:
+ raise ValidationError("You cant update an avatar image that is not primary")
+
+ if instance.primary is True:
+ # Find the next avatar, and set it as the new primary
+ set_new_primary(self.get_queryset(), instance)
+
+ self.perform_update(serializer)
+ invalidate_cache(request.user)
+ message = _("Successfully updated your avatar.")
+ if getattr(instance, "_prefetched_objects_cache", None):
+ # If 'prefetch_related' has been applied to a queryset, we need to
+ # forcibly invalidate the prefetch cache on the instance.
+ instance._prefetched_objects_cache = {}
+ context_data = {"message": message, "data": serializer.data}
+ return Response(context_data)
+
+ @action(
+ ["GET"], detail=False, url_path="render_primary", name="Render Primary Avatar"
+ )
+ def render_primary(self, request, *args, **kwargs):
+ """
+
+ URL Example :
+
+ 1 - render_primary/
+ 2 - render_primary/?width=400 or render_primary/?height=400
+ 3 - render_primary/?width=500&height=400
+ """
+ context_data = {}
+ avatar_size = assign_width_or_height(request.query_params)
+
+ width = avatar_size.get("width")
+ height = avatar_size.get("height")
+
+ primary_avatar = get_primary_avatar(request.user, width=width, height=height)
+
+ if primary_avatar and primary_avatar.primary:
+ url = primary_avatar.avatar_url(width, height)
+
+ else:
+ url = get_default_avatar_url()
+ if bool(request.query_params):
+ context_data.update(
+ {"message": "Resize parameters not working for default avatar"}
+ )
+
+ context_data.update({"image_url": request.build_absolute_uri(url)})
+ return Response(context_data)
diff --git a/test_proj/test_proj/settings.py b/test_proj/test_proj/settings.py
index cd24a2b..ea8507d 100644
--- a/test_proj/test_proj/settings.py
+++ b/test_proj/test_proj/settings.py
@@ -38,6 +38,7 @@ INSTALLED_APPS = [
"django.contrib.messages",
"django.contrib.staticfiles",
"avatar",
+ "rest_framework",
]
MIDDLEWARE = [
diff --git a/test_proj/test_proj/urls.py b/test_proj/test_proj/urls.py
index e874078..93b0308 100644
--- a/test_proj/test_proj/urls.py
+++ b/test_proj/test_proj/urls.py
@@ -1,16 +1,18 @@
from django.conf import settings
-from django.conf.urls import include, url
+from django.conf.urls import include
from django.contrib import admin
+from django.urls import re_path
from django.views.static import serve
urlpatterns = [
- url(r"^admin/", admin.site.urls),
- url(r"^avatar/", include("avatar.urls")),
+ re_path(r"^admin/", admin.site.urls),
+ re_path(r"^avatar/", include("avatar.urls")),
+ re_path(r"^api/", include("avatar.api.urls")),
]
if settings.DEBUG:
# static files (images, css, javascript, etc.)
urlpatterns += [
- url(r"^media/(?P.*)$", serve, {"document_root": settings.MEDIA_ROOT})
+ re_path(r"^media/(?P.*)$", serve, {"document_root": settings.MEDIA_ROOT})
]