diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..108d381 --- /dev/null +++ b/.gitignore @@ -0,0 +1,100 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +env/ +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*,cover +.hypothesis/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# pyenv +.python-version + +# celery beat schedule file +celerybeat-schedule + +# SageMath parsed files +*.sage.py + +# dotenv +.env + +# virtualenv +.venv +venv/ +ENV/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +*.sqlite3 diff --git a/README.md b/README.md index 278ee81..9b4ffa7 100644 --- a/README.md +++ b/README.md @@ -1 +1,43 @@ -# Pip-Version-Viewer \ No newline at end of file +# Django Version Viewer + +The pip package version viewer plugin allows a queryable endpoint to display a list of dicts representing all installed pip packages in the environment that django is running in. It also allows the insertion of a template tag to any template to display a link which calls up a pop up modal displaying all installed pip packages. You may also configure which users have access to the link and endpoint. + +--------------------------------------- +## Installation +--------------------------------------- + +Add the following to `INSTALLED_APPS` in `settings.py` + + INSTALLED_APPS = [ + 'django_version_viewer' + ] + +Add `django_version_viewer` include to `urls.py` + + urlpatterns = [ + ... + url(r'^django_version_viewer/', include('django_version_viewer.urls')), + ... + ] + +You can set your own access permissions on the template tag and route by defining your own +`Accessor` class. This class must have a `allow_access` method that returns a `boolean`. By defualt, +django_version_viewer only allows superusers access to the route and template tag. + + # Django Version Viewer settings: + # default class only allows superusers access + ACCESSOR_CLASS_PATH = 'mypathto.my.AccessorClass' + + +Override the `base.html` django Admin template (or the template of your choosing) by creating a `base.html` file inside a `templates/admin` directory in your project. + +Make sure you insert the necessary `src` and `link` blocks so that the popup modal works properly. + + + + + +Finally, load the template tags file and insert the template tag where ever you want the "Installed Versions" link to show up: + + {% load pip_version_viewer_tags %} + {% show_pip_package_versions %} diff --git a/django_version_viewer/__init__.py b/django_version_viewer/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/django_version_viewer/mixins.py b/django_version_viewer/mixins.py new file mode 100644 index 0000000..e65d6c5 --- /dev/null +++ b/django_version_viewer/mixins.py @@ -0,0 +1,6 @@ + + +class Accessor(object): + + def allow_access(self, request): + return request.user.is_superuser diff --git a/django_version_viewer/pip_viewer.py b/django_version_viewer/pip_viewer.py new file mode 100644 index 0000000..f217da3 --- /dev/null +++ b/django_version_viewer/pip_viewer.py @@ -0,0 +1,8 @@ +import pip +from operator import itemgetter + + +def list_package_versions(): + installed_packages = pip.get_installed_distributions() + results = [{"package_name": i.key, "package_version": i.version} for i in installed_packages] + return sorted(results, key=itemgetter('package_name')) diff --git a/django_version_viewer/templates/version_viewer.html b/django_version_viewer/templates/version_viewer.html new file mode 100644 index 0000000..8c66b1b --- /dev/null +++ b/django_version_viewer/templates/version_viewer.html @@ -0,0 +1,56 @@ +{% block content %} +
+ {% if allow %} + Installed Versions + {% endif %} +
+ + + + +{% endblock %} + + +{% block javascript %} + +{% endblock %} diff --git a/django_version_viewer/templatetags/__init__.py b/django_version_viewer/templatetags/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/django_version_viewer/templatetags/pip_version_viewer_tags.py b/django_version_viewer/templatetags/pip_version_viewer_tags.py new file mode 100644 index 0000000..c2a3f66 --- /dev/null +++ b/django_version_viewer/templatetags/pip_version_viewer_tags.py @@ -0,0 +1,18 @@ +from pydoc import locate + +from django.conf import settings +from django import template + + +register = template.Library() +accessor_class = locate( + getattr(settings, 'ACCESSOR_CLASS_PATH', 'django_version_viewer.mixins.Accessor')) +accessor = accessor_class() + + +@register.inclusion_tag('version_viewer.html', takes_context=True) +def show_pip_package_versions(context): + if accessor.allow_access(request=context['request']): + return {'allow': True} + else: + return {'allow': False} diff --git a/django_version_viewer/urls.py b/django_version_viewer/urls.py new file mode 100644 index 0000000..ea076e5 --- /dev/null +++ b/django_version_viewer/urls.py @@ -0,0 +1,12 @@ +try: + # django 1.6+ + from django.conf.urls import url +except ImportError: + # django <1.6 + from django.conf.urls.defaults import url + +from . import views + +urlpatterns = [ + url(r'^$', views.DjangoVersionViewer.as_view(), name='django_version_viewer'), +] diff --git a/django_version_viewer/views.py b/django_version_viewer/views.py new file mode 100644 index 0000000..e72e49d --- /dev/null +++ b/django_version_viewer/views.py @@ -0,0 +1,21 @@ +from pydoc import locate + +from django.http import HttpResponse +from django.conf import settings +from django.core.exceptions import PermissionDenied +from pip_viewer import list_package_versions +from django.views.generic import View +import json + +accessor_class = locate( + getattr(settings, 'ACCESSOR_CLASS_PATH', 'django_version_viewer.mixins.Accessor')) +accessor = accessor_class() + + +class DjangoVersionViewer(View): + + def get(self, request, *args, **kwargs): + if not accessor.allow_access(request): + raise PermissionDenied + packages = list_package_versions() + return HttpResponse(json.dumps(packages), status=200, content_type="application/json") diff --git a/example18/example18/__init__.py b/example18/example18/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/example18/example18/settings.py b/example18/example18/settings.py new file mode 100644 index 0000000..160d607 --- /dev/null +++ b/example18/example18/settings.py @@ -0,0 +1,106 @@ +""" +Django settings for example18 project. + +Generated by 'django-admin startproject' using Django 1.8.18. + +For more information on this file, see +https://docs.djangoproject.com/en/1.8/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/1.8/ref/settings/ +""" + +# Build paths inside the project like this: os.path.join(BASE_DIR, ...) +import os + +BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +PROJECT_DIR = os.path.join(BASE_DIR, "example") + + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/1.8/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = '*2$rhmg25-#et_b5nt)_oq*u5ymb!xo0*2xl#5(-!h#)m@&bqq' + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = True + +ALLOWED_HOSTS = [] + + +# Application definition + +INSTALLED_APPS = ( + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + + 'django_version_viewer' +) + +MIDDLEWARE_CLASSES = ( + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.auth.middleware.SessionAuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', + 'django.middleware.security.SecurityMiddleware', +) + +ROOT_URLCONF = 'example18.urls' +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [os.path.join(BASE_DIR, 'templates'), ], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + ], + }, + }, +] + +WSGI_APPLICATION = 'example18.wsgi.application' + + +# Database +# https://docs.djangoproject.com/en/1.8/ref/settings/#databases + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), + } +} + + +# Internationalization +# https://docs.djangoproject.com/en/1.8/topics/i18n/ + +LANGUAGE_CODE = 'en-us' + +TIME_ZONE = 'UTC' + +USE_I18N = True + +USE_L10N = True + +USE_TZ = True + + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/1.8/howto/static-files/ + +STATIC_URL = '/static/' + +ACCESSOR_CLASS_PATH = 'django_version_viewer.mixins.Accessor' diff --git a/example18/example18/tests.py b/example18/example18/tests.py new file mode 100644 index 0000000..a768d32 --- /dev/null +++ b/example18/example18/tests.py @@ -0,0 +1,66 @@ +import mock +import unittest +import json +from django.core.urlresolvers import reverse +from django.contrib.auth.models import User +from django.test import Client + + +class TestVersionViewer(unittest.TestCase): + + url_django_version_viewer = reverse('django_version_viewer') + + mock_data = [ + {"key": "appdirs", "version": "1.4.3"}, + {"key": "django", "version": "1.8.18"}, + {"key": "six", "version": "1.10.0"} + ] + + def mocked_pip_get_installed_distributions(self, *args, **kwargs): + class MockPipObject: + def __init__(self, key, version): + self.key = key + self.version = version + + class MockResponse: + def __init__(self, data): + self.packages = [] + for _dict in data: + mocked_obj = MockPipObject(_dict['key'], _dict['version']) + self.packages.append(mocked_obj) + + def get_installed_distributions(self): + return self.packages + + return MockResponse(self.mock_data).get_installed_distributions() + + def setUp(self): + self.admin = User.objects.create_superuser( + email="adminmail@mail.com", + username="admin_user", + password="password", + ) + self.user = User.objects.create( + email="user@usermail.com", + username="regular_user", + password="password" + ) + + def tearDown(self): + self.admin.delete() + self.user.delete() + + def test_django_version_viewer_view_admin(self): + client = Client() + client.login(username=self.admin.username, password="password") + with mock.patch('pip.get_installed_distributions', side_effect=self.mocked_pip_get_installed_distributions): + response = client.get(self.url_django_version_viewer) + json_response = json.loads(response.content) + self.assertEqual(response.status_code, 200) + self.assertEqual(len(json_response), 3) + + def test_django_version_viewer_view_user(self): + client = Client() + client.login(username=self.user.username, password="password") + response = client.get(self.url_django_version_viewer) + self.assertEqual(response.status_code, 403) diff --git a/example18/example18/urls.py b/example18/example18/urls.py new file mode 100644 index 0000000..ca3f27e --- /dev/null +++ b/example18/example18/urls.py @@ -0,0 +1,21 @@ +"""example18 URL Configuration + +The `urlpatterns` list routes URLs to views. For more information please see: + https://docs.djangoproject.com/en/1.8/topics/http/urls/ +Examples: +Function views + 1. Add an import: from my_app import views + 2. Add a URL to urlpatterns: url(r'^$', views.home, name='home') +Class-based views + 1. Add an import: from other_app.views import Home + 2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home') +Including another URLconf + 1. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls')) +""" +from django.conf.urls import include, url +from django.contrib import admin + +urlpatterns = [ + url(r'^admin/', include(admin.site.urls)), + url(r'^django_version_viewer/', include('django_version_viewer.urls')), +] diff --git a/example18/example18/wsgi.py b/example18/example18/wsgi.py new file mode 100644 index 0000000..e50407d --- /dev/null +++ b/example18/example18/wsgi.py @@ -0,0 +1,16 @@ +""" +WSGI config for example18 project. + +It exposes the WSGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/1.8/howto/deployment/wsgi/ +""" + +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "example18.settings") + +application = get_wsgi_application() diff --git a/example18/manage.py b/example18/manage.py new file mode 100755 index 0000000..d1178cb --- /dev/null +++ b/example18/manage.py @@ -0,0 +1,10 @@ +#!/usr/bin/env python +import os +import sys + +if __name__ == "__main__": + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "example18.settings") + + from django.core.management import execute_from_command_line + + execute_from_command_line(sys.argv) diff --git a/example18/requirements.txt b/example18/requirements.txt new file mode 100644 index 0000000..3c05aa7 --- /dev/null +++ b/example18/requirements.txt @@ -0,0 +1,2 @@ +-r ./requirements/pypi_requirements.txt +-e .. # install with containing version_viewer version diff --git a/example18/requirements/pypi_requirements.txt b/example18/requirements/pypi_requirements.txt new file mode 100644 index 0000000..3b2d054 --- /dev/null +++ b/example18/requirements/pypi_requirements.txt @@ -0,0 +1,2 @@ +Django==1.8.18 +mock==2.0.0 diff --git a/example18/templates/admin/base.html b/example18/templates/admin/base.html new file mode 100644 index 0000000..f60f3d5 --- /dev/null +++ b/example18/templates/admin/base.html @@ -0,0 +1,92 @@ +{% load i18n admin_static pip_version_viewer_tags %} +{% get_current_language as LANGUAGE_CODE %}{% get_current_language_bidi as LANGUAGE_BIDI %} + + +{% block title %}{% endblock %} + + +{% block extrastyle %}{% endblock %} + +{% if LANGUAGE_BIDI %}{% endif %} + + + + +{% block extrahead %}{% endblock %} +{% block blockbots %}{% endblock %} + +{% load i18n %} + + + + +
+ {% if not is_popup %} + + + + {% block breadcrumbs %} + + {% endblock %} + {% endif %} + + {% block messages %} + {% if messages %} + + {% endif %} + {% endblock messages %} + + +
+ {% block pretitle %}{% show_pip_package_versions %}{% endblock %} + {% block content_title %}{% if title %}

{{ title }}

{% endif %}{% endblock %} + {% block content %} + {% block object-tools %}{% endblock %} + {{ content }} + {% endblock %} + {% block sidebar %}{% endblock %} +
+
+ + + {% block footer %}{% endblock %} +
+ + + + diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..7b7a146 --- /dev/null +++ b/setup.py @@ -0,0 +1,16 @@ +from setuptools import setup + +setup( + name='django_version_viewer', + version='0.0.1a1', + packages=['django_version_viewer'], + install_requires=( + ), + description="Django app for viewing pip packages and their versions", + long_description=open('README.md', 'r').read(), + classifiers=[ + 'Programming Language :: Python :: 2.7', + 'Framework :: Django', + 'Framework :: Django :: 1.8' + ] +)