Browse Source

Initial publishing

master
parent
commit
8ed011e5e0
Signed by: Orhideous <[email protected]> GPG Key ID: 62E078AB621B0D15
38 changed files with 1523 additions and 0 deletions
  1. +5
    -0
      HISTORY.md
  2. +16
    -0
      MANIFEST.in
  3. +87
    -0
      Makefile
  4. +13
    -0
      lavender/__init__.py
  5. +85
    -0
      lavender/admin.py
  6. +5
    -0
      lavender/apps.py
  7. +240
    -0
      lavender/fixtures/auth.yaml
  8. +50
    -0
      lavender/fixtures/contenttypes.yaml
  9. +89
    -0
      lavender/fixtures/lavender.yaml
  10. +84
    -0
      lavender/forms.py
  11. +95
    -0
      lavender/migrations/0001_initial.py
  12. +23
    -0
      lavender/migrations/0002_add_token_uuid_and_server_id.py
  13. +18
    -0
      lavender/migrations/0003_increase_server_id_length.py
  14. +25
    -0
      lavender/migrations/0004_quenta.py
  15. +33
    -0
      lavender/migrations/0005_wardrobe.py
  16. +18
    -0
      lavender/migrations/0006_blank_packages.py
  17. +27
    -0
      lavender/migrations/0007_fix_gamelog.py
  18. +0
    -0
      lavender/migrations/__init__.py
  19. +101
    -0
      lavender/models.py
  20. +6
    -0
      lavender/settings.py
  21. +22
    -0
      lavender/texture.py
  22. +42
    -0
      lavender/urls.py
  23. +36
    -0
      lavender/utils.py
  24. +0
    -0
      lavender/views/__init__.py
  25. +0
    -0
      lavender/views/api/__init__.py
  26. +56
    -0
      lavender/views/api/authentication.py
  27. +39
    -0
      lavender/views/api/decorator.py
  28. +38
    -0
      lavender/views/api/packages.py
  29. +27
    -0
      lavender/views/api/telemetry.py
  30. +70
    -0
      lavender/views/profile.py
  31. +29
    -0
      lavender/views/registration.py
  32. +21
    -0
      manage.py
  33. +26
    -0
      pylintrc
  34. +6
    -0
      requirements.txt
  35. +11
    -0
      requirements_dev.txt
  36. +33
    -0
      setup.cfg
  37. +47
    -0
      setup.py
  38. +0
    -0
      tests/__init__.py

+ 5
- 0
HISTORY.md View File

@@ -0,0 +1,5 @@
# History

## 0.8.0 (2020-01-02)

* First public release on PyPI.

+ 16
- 0
MANIFEST.in View File

@@ -0,0 +1,16 @@
graft lavender
graft tests

include LICENSE
include README.md
include HISTORY.md

include requirements.txt
include requirements_dev.txt

include tox.ini
include pylintrc
include Makefile

global-exclude *.py[co] __pycache__
recursive-include docs *.rst conf.py Makefile *.jpg *.png *.gif

+ 87
- 0
Makefile View File

@@ -0,0 +1,87 @@
.PHONY: clean clean-test clean-pyc clean-build docs help
.DEFAULT_GOAL := help

define BROWSER_PYSCRIPT
import os, webbrowser, sys

from urllib.request import pathname2url

webbrowser.open("file://" + pathname2url(os.path.abspath(sys.argv[1])))
endef
export BROWSER_PYSCRIPT

define PRINT_HELP_PYSCRIPT
import re, sys

for line in sys.stdin:
match = re.match(r'^([a-zA-Z_-]+):.*?## (.*)$$', line)
if match:
target, help = match.groups()
print("%-20s %s" % (target, help))
endef
export PRINT_HELP_PYSCRIPT

BROWSER := python -c "$$BROWSER_PYSCRIPT"

help:
@python -c "$$PRINT_HELP_PYSCRIPT" < $(MAKEFILE_LIST)

clean: clean-build clean-pyc clean-test ## remove all build, test, coverage and Python artifacts

clean-build: ## remove build artifacts
rm -fr build/
rm -fr dist/
rm -fr .eggs/
find . -name '*.egg-info' -exec rm -fr {} +
find . -name '*.egg' -exec rm -fr {} +

clean-pyc: ## remove Python file artifacts
find . -name '*.pyc' -exec rm -f {} +
find . -name '*.pyo' -exec rm -f {} +
find . -name '*~' -exec rm -f {} +
find . -name '__pycache__' -exec rm -fr {} +

clean-test: ## remove test and coverage artifacts
rm -fr .tox/
rm -f .coverage
rm -fr htmlcov/
rm -fr .pytest_cache
rm -fr lavender/static

lint: ## check style with flake8
pylint lavender
flake8 lavender tests

test: ## run tests quickly with the default Python
python setup.py test

test-all: ## run tests on every Python version with tox
tox

coverage: ## check code coverage quickly with the default Python
coverage run --source lavender_nidhoggr -m pytest
coverage report -m
coverage html
$(BROWSER) htmlcov/index.html

docs: ## generate Sphinx HTML documentation, including API docs
rm -f docs/lavender_nidhoggr.rst
rm -f docs/modules.rst
sphinx-apidoc -o docs/generated lavender_nidhoggr
$(MAKE) -C docs clean
$(MAKE) -C docs html
$(BROWSER) docs/_build/html/index.html

servedocs: docs ## compile the docs watching for changes
watchmedo shell-command -p '*.rst' -c '$(MAKE) -C docs html' -R -D .

release: dist ## package and upload a release
twine upload dist/*

dist: clean ## builds source and wheel package
python setup.py sdist
python setup.py bdist_wheel
ls -l dist

install: clean ## install the package to the active Python's site-packages
python setup.py install

+ 13
- 0
lavender/__init__.py View File

@@ -0,0 +1,13 @@
from pkg_resources import get_distribution, DistributionNotFound
import os.path

try:
_dist = get_distribution('lavender')
dist_loc = os.path.normcase(_dist.location)
here = os.path.normcase(__file__)
if not here.startswith(os.path.join(dist_loc, 'lavender')):
raise DistributionNotFound
except DistributionNotFound:
__version__ = 'Please install this project with setup.py'
else:
__version__ = _dist.version

+ 85
- 0
lavender/admin.py View File

@@ -0,0 +1,85 @@
from django.contrib import admin
from django.contrib.admin import ModelAdmin
from django.contrib.auth.admin import UserAdmin
from django.utils.translation import ugettext_lazy as _

from lavender.models import Package, Player, LogHistory, GameLog, Quenta, Wardrobe


class PlayerAdmin(UserAdmin):
filter_horizontal = ('packages',)

def get_fieldsets(self, request, obj=None):
if not obj:
return self.add_fieldsets

if request.user.is_superuser:
perm_fields = ('is_active', 'is_staff', 'is_superuser',
'groups', 'user_permissions')
else:
perm_fields = ('is_active', 'is_staff')

return [
(None, {'fields': ('username', 'password')}),
(_('Personal info'), {'fields': ('first_name', 'last_name', 'email')}),
(_('Permissions'), {'fields': perm_fields}),
('Minecraft', {'fields': ('packages',)})
]


class LogHistoryAdmin(ModelAdmin):
list_filter = ('date', 'source')
list_display = ('player', 'date', 'source')
search_fields = (
'player__username',
)

def get_queryset(self, request):
return super().get_queryset(request).select_related('player')


class GameLogAdmin(ModelAdmin):
list_display = ('player', 'date', 'kind',)
list_filter = ('date', 'kind')
search_fields = (
'player__username',
'kind'
)

def get_queryset(self, request):
return super().get_queryset(request).select_related('player')


class QuentaAdmin(ModelAdmin):
list_display = ('player', 'approved',)
list_filter = ('approved',)
search_fields = (
'player__username',
)

def get_queryset(self, request):
return super().get_queryset(request).select_related('player')


class WardrobeAdmin(ModelAdmin):
list_display = ('player',)
fields = ('player', 'skin', 'cloak', 'elytra')
search_fields = (
'player__username',
)

def get_queryset(self, request):
return super().get_queryset(request).select_related('player')


class PackageAdmin(ModelAdmin):
list_display = ('name', 'version', 'title', 'location', 'is_default')
list_filter = ('is_default',)


admin.site.register(Player, PlayerAdmin)
admin.site.register(LogHistory, LogHistoryAdmin)
admin.site.register(GameLog, GameLogAdmin)
admin.site.register(Quenta, QuentaAdmin)
admin.site.register(Wardrobe, WardrobeAdmin)
admin.site.register(Package, PackageAdmin)

+ 5
- 0
lavender/apps.py View File

@@ -0,0 +1,5 @@
from django.apps import AppConfig


class LavenderConfig(AppConfig):
name = 'lavender'

+ 240
- 0
lavender/fixtures/auth.yaml View File

@@ -0,0 +1,240 @@
- model: auth.permission
pk: 1
fields:
name: Can add user
content_type: 1
codename: add_player
- model: auth.permission
pk: 2
fields:
name: Can change user
content_type: 1
codename: change_player
- model: auth.permission
pk: 3
fields:
name: Can delete user
content_type: 1
codename: delete_player
- model: auth.permission
pk: 4
fields:
name: Can view user
content_type: 1
codename: view_player
- model: auth.permission
pk: 5
fields:
name: Can add package
content_type: 2
codename: add_package
- model: auth.permission
pk: 6
fields:
name: Can change package
content_type: 2
codename: change_package
- model: auth.permission
pk: 7
fields:
name: Can delete package
content_type: 2
codename: delete_package
- model: auth.permission
pk: 8
fields:
name: Can view package
content_type: 2
codename: view_package
- model: auth.permission
pk: 9
fields:
name: Can add token
content_type: 3
codename: add_token
- model: auth.permission
pk: 10
fields:
name: Can change token
content_type: 3
codename: change_token
- model: auth.permission
pk: 11
fields:
name: Can delete token
content_type: 3
codename: delete_token
- model: auth.permission
pk: 12
fields:
name: Can view token
content_type: 3
codename: view_token
- model: auth.permission
pk: 13
fields:
name: Can add log history
content_type: 4
codename: add_loghistory
- model: auth.permission
pk: 14
fields:
name: Can change log history
content_type: 4
codename: change_loghistory
- model: auth.permission
pk: 15
fields:
name: Can delete log history
content_type: 4
codename: delete_loghistory
- model: auth.permission
pk: 16
fields:
name: Can view log history
content_type: 4
codename: view_loghistory
- model: auth.permission
pk: 17
fields:
name: Can add game log
content_type: 5
codename: add_gamelog
- model: auth.permission
pk: 18
fields:
name: Can change game log
content_type: 5
codename: change_gamelog
- model: auth.permission
pk: 19
fields:
name: Can delete game log
content_type: 5
codename: delete_gamelog
- model: auth.permission
pk: 20
fields:
name: Can view game log
content_type: 5
codename: view_gamelog
- model: auth.permission
pk: 21
fields:
name: Can add log entry
content_type: 6
codename: add_logentry
- model: auth.permission
pk: 22
fields:
name: Can change log entry
content_type: 6
codename: change_logentry
- model: auth.permission
pk: 23
fields:
name: Can delete log entry
content_type: 6
codename: delete_logentry
- model: auth.permission
pk: 24
fields:
name: Can view log entry
content_type: 6
codename: view_logentry
- model: auth.permission
pk: 25
fields:
name: Can add permission
content_type: 7
codename: add_permission
- model: auth.permission
pk: 26
fields:
name: Can change permission
content_type: 7
codename: change_permission
- model: auth.permission
pk: 27
fields:
name: Can delete permission
content_type: 7
codename: delete_permission
- model: auth.permission
pk: 28
fields:
name: Can view permission
content_type: 7
codename: view_permission
- model: auth.permission
pk: 29
fields:
name: Can add group
content_type: 8
codename: add_group
- model: auth.permission
pk: 30
fields:
name: Can change group
content_type: 8
codename: change_group
- model: auth.permission
pk: 31
fields:
name: Can delete group
content_type: 8
codename: delete_group
- model: auth.permission
pk: 32
fields:
name: Can view group
content_type: 8
codename: view_group
- model: auth.permission
pk: 33
fields:
name: Can add content type
content_type: 9
codename: add_contenttype
- model: auth.permission
pk: 34
fields:
name: Can change content type
content_type: 9
codename: change_contenttype
- model: auth.permission
pk: 35
fields:
name: Can delete content type
content_type: 9
codename: delete_contenttype
- model: auth.permission
pk: 36
fields:
name: Can view content type
content_type: 9
codename: view_contenttype
- model: auth.permission
pk: 37
fields:
name: Can add session
content_type: 10
codename: add_session
- model: auth.permission
pk: 38
fields:
name: Can change session
content_type: 10
codename: change_session
- model: auth.permission
pk: 39
fields:
name: Can delete session
content_type: 10
codename: delete_session
- model: auth.permission
pk: 40
fields:
name: Can view session
content_type: 10
codename: view_session

+ 50
- 0
lavender/fixtures/contenttypes.yaml View File

@@ -0,0 +1,50 @@
- model: contenttypes.contenttype
pk: 1
fields:
app_label: lavender
model: player
- model: contenttypes.contenttype
pk: 2
fields:
app_label: lavender
model: package
- model: contenttypes.contenttype
pk: 3
fields:
app_label: lavender
model: token
- model: contenttypes.contenttype
pk: 4
fields:
app_label: lavender
model: loghistory
- model: contenttypes.contenttype
pk: 5
fields:
app_label: lavender
model: gamelog
- model: contenttypes.contenttype
pk: 6
fields:
app_label: admin
model: logentry
- model: contenttypes.contenttype
pk: 7
fields:
app_label: auth
model: permission
- model: contenttypes.contenttype
pk: 8
fields:
app_label: auth
model: group
- model: contenttypes.contenttype
pk: 9
fields:
app_label: contenttypes
model: contenttype
- model: contenttypes.contenttype
pk: 10
fields:
app_label: sessions
model: session

+ 89
- 0
lavender/fixtures/lavender.yaml View File

@@ -0,0 +1,89 @@
- model: lavender.player
pk: 1
fields:
password: pbkdf2_sha256$150000$LwVawqypT6vQ$yp0Ix6EJaz9bGK+cwNIZdYjCRyuMm3wv95PmWTpiDts=
last_login: 2019-06-25 00:27:43.781438+00:00
is_superuser: true
username: admin
first_name: ''
last_name: ''
email: [email protected]
is_staff: true
is_active: true
date_joined: 2019-06-24 20:38:47+00:00
groups: []
user_permissions: []
packages:
- 1
- model: lavender.player
pk: 2
fields:
password: pbkdf2_sha256$150000$ecUA3TkyyOsx$dqC+GlRSDFYhejCql2dXSj0lIUk7VpZkvQ8y0VogpSk=
last_login: 2019-06-25 00:27:25.144537+00:00
is_superuser: false
username: user
first_name: ''
last_name: ''
email: [email protected]
is_staff: false
is_active: true
date_joined: 2019-06-25 00:27:24.365738+00:00
groups: []
user_permissions: []
packages:
- 1
- model: lavender.loghistory
pk: 1
fields:
player: 1
date: 2019-06-24 23:50:59.029372+00:00
source: site
- model: lavender.loghistory
pk: 2
fields:
player: 2
date: 2019-06-25 00:27:25.121134+00:00
source: site
- model: lavender.loghistory
pk: 3
fields:
player: 1
date: 2019-06-25 00:27:43.775183+00:00
source: site
- model: lavender.token
pk: 1
fields:
player: 1
access: '1'
client: '1'
created: 2019-06-25 00:31:21.656000+00:00
- model: lavender.token
pk: 2
fields:
player: 2
access: '2'
client: '2'
created: 2019-06-25 00:31:37.512000+00:00
- model: lavender.gamelog
pk: 1
fields:
player: 1
date: 2019-06-25 00:11:07+00:00
log_type: test
url: https://paste.mc4ep.org/podinuzowa
- model: lavender.package
pk: 1
fields:
name: altiore
version: '1.0'
title: MC4EP 1.12.2
location: https://packages.mc4ep.org/altiore/index.json
is_default: true
- model: lavender.package
pk: 2
fields:
name: somnium
version: '1.0'
title: MC 1.7.10
location: https://packages.mc4ep.org/somnium/index.json
is_default: false

+ 84
- 0
lavender/forms.py View File

@@ -0,0 +1,84 @@
from django import forms
from django.contrib.auth import get_user_model
from django.contrib.auth.forms import UserCreationForm

from lavender.utils import check_nickname
from lavender.models import Quenta, Wardrobe

User = get_user_model()


class RegistrationForm(UserCreationForm):
email = forms.EmailField(max_length=75, required=True)
consent = forms.BooleanField(required=True)

class Meta:
model = User
fields = ('username', 'email',)

def clean_username(self):
username = self.cleaned_data['username']
errors = check_nickname(username)
if len(errors) > 0:
raise forms.ValidationError(errors)

return username

def clean_consent(self):
pass

def save(self, commit=True):
user = super(RegistrationForm, self).save(commit=False)
user.email = self.cleaned_data["email"]
if commit:
user.save()
return user


class EmailChangeForm(forms.ModelForm):
email = forms.EmailField(max_length=75, required=False)

class Meta:
model = User
fields = ('email',)

def clean_email(self):
email = self.cleaned_data['email']
if email:
if User.objects.exclude(username=self.instance.username).filter(email=email).exists():
raise forms.ValidationError('Этот адрес почты уже используется')
return email


class QuentaForm(forms.ModelForm):
class Meta:
model = Quenta
fields = ('text',)

def __init__(self, *args, **kwargs):
self.user = kwargs.pop("user")
super().__init__(*args, **kwargs)

def save(self, commit=True):
quenta: Quenta = super().save(commit=False)
quenta.player = self.user
if commit:
quenta.save()
return quenta


class WardrobeForm(forms.ModelForm):
class Meta:
model = Wardrobe
fields = ('skin', 'cloak', 'elytra')

def __init__(self, *args, **kwargs):
self.user = kwargs.pop("user")
super().__init__(*args, **kwargs)

def save(self, commit=True):
wardrobe: Wardrobe = super().save(commit=False)
wardrobe.player = self.user
if commit:
wardrobe.save()
return wardrobe

+ 95
- 0
lavender/migrations/0001_initial.py View File

@@ -0,0 +1,95 @@
# Generated by Django 2.2.1 on 2019-06-24 23:37

from django.conf import settings
import django.contrib.auth.models
import django.contrib.auth.validators
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone


class Migration(migrations.Migration):

initial = True

dependencies = [
('auth', '0011_update_proxy_permissions'),
]

operations = [
migrations.CreateModel(
name='Player',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('password', models.CharField(max_length=128, verbose_name='password')),
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')),
('first_name', models.CharField(blank=True, max_length=30, verbose_name='first name')),
('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')),
('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')),
('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')),
('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.Group', verbose_name='groups')),
],
options={
'verbose_name': 'user',
'verbose_name_plural': 'users',
'abstract': False,
},
managers=[
('objects', django.contrib.auth.models.UserManager()),
],
),
migrations.CreateModel(
name='Package',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=16)),
('version', models.CharField(max_length=16)),
('title', models.CharField(max_length=255)),
('location', models.CharField(max_length=255)),
('is_default', models.BooleanField(default=False)),
],
),
migrations.CreateModel(
name='Token',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('access', models.CharField(max_length=32, null=True)),
('client', models.CharField(max_length=32, null=True)),
('created', models.DateTimeField(default=django.utils.timezone.now)),
('player', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
),
migrations.CreateModel(
name='LogHistory',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('date', models.DateTimeField(default=django.utils.timezone.now)),
('source', models.CharField(max_length=255)),
('player', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
),
migrations.CreateModel(
name='GameLog',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('date', models.DateTimeField(default=django.utils.timezone.now)),
('log_type', models.CharField(max_length=16)),
('url', models.CharField(max_length=64)),
('player', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
),
migrations.AddField(
model_name='player',
name='packages',
field=models.ManyToManyField(to='lavender.Package'),
),
migrations.AddField(
model_name='player',
name='user_permissions',
field=models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.Permission', verbose_name='user permissions'),
),
]

+ 23
- 0
lavender/migrations/0002_add_token_uuid_and_server_id.py View File

@@ -0,0 +1,23 @@
# Generated by Django 2.2.1 on 2019-06-25 18:15

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('lavender', '0001_initial'),
]

operations = [
migrations.AddField(
model_name='token',
name='server_id',
field=models.CharField(max_length=32, null=True),
),
migrations.AddField(
model_name='token',
name='uuid',
field=models.CharField(max_length=32, null=True),
),
]

+ 18
- 0
lavender/migrations/0003_increase_server_id_length.py View File

@@ -0,0 +1,18 @@
# Generated by Django 2.2.1 on 2019-06-25 18:21

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('lavender', '0002_add_token_uuid_and_server_id'),
]

operations = [
migrations.AlterField(
model_name='token',
name='server_id',
field=models.CharField(max_length=48, null=True),
),
]

+ 25
- 0
lavender/migrations/0004_quenta.py View File

@@ -0,0 +1,25 @@
# Generated by Django 2.2.1 on 2019-12-08 23:40

from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

dependencies = [
('lavender', '0003_increase_server_id_length'),
]

operations = [
migrations.CreateModel(
name='Quenta',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('text', models.TextField()),
('comments', models.TextField(blank=True)),
('approved', models.BooleanField(default=False)),
('player', models.OneToOneField(null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
),
]

+ 33
- 0
lavender/migrations/0005_wardrobe.py View File

@@ -0,0 +1,33 @@
# Generated by Django 2.2.1 on 2019-12-09 00:47

from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import functools
import lavender.texture


class Migration(migrations.Migration):

dependencies = [
('lavender', '0004_quenta'),
]

operations = [
migrations.CreateModel(
name='Wardrobe',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('skin_height', models.PositiveIntegerField(null=True)),
('skin_width', models.PositiveIntegerField(null=True)),
('skin', models.ImageField(blank=True, height_field='skin_height', null=True, storage=lavender.texture.OverwriteStorage(), upload_to=functools.partial(lavender.texture.path_handler, *(lavender.texture.TextureType('skin'),), **{}), width_field='skin_width')),
('cloak_height', models.PositiveIntegerField(null=True)),
('cloak_width', models.PositiveIntegerField(null=True)),
('cloak', models.ImageField(blank=True, height_field='cloak_height', null=True, storage=lavender.texture.OverwriteStorage(), upload_to=functools.partial(lavender.texture.path_handler, *(lavender.texture.TextureType('cloak'),), **{}), width_field='cloak_width')),
('elytra_height', models.PositiveIntegerField(null=True)),
('elytra_width', models.PositiveIntegerField(null=True)),
('elytra', models.ImageField(blank=True, height_field='elytra_height', null=True, storage=lavender.texture.OverwriteStorage(), upload_to=functools.partial(lavender.texture.path_handler, *(lavender.texture.TextureType('elytra'),), **{}), width_field='elytra_width')),
('player', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
),
]

+ 18
- 0
lavender/migrations/0006_blank_packages.py View File

@@ -0,0 +1,18 @@
# Generated by Django 2.2.1 on 2019-12-14 18:15

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('lavender', '0005_wardrobe'),
]

operations = [
migrations.AlterField(
model_name='player',
name='packages',
field=models.ManyToManyField(blank=True, to='lavender.Package'),
),
]

+ 27
- 0
lavender/migrations/0007_fix_gamelog.py View File

@@ -0,0 +1,27 @@
# Generated by Django 2.2.1 on 2020-01-02 16:57

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('lavender', '0006_blank_packages'),
]

operations = [
migrations.RenameField(
model_name='gamelog',
old_name='log_type',
new_name='kind',
),
migrations.RemoveField(
model_name='gamelog',
name='url',
),
migrations.AddField(
model_name='gamelog',
name='payload',
field=models.TextField(default=''),
),
]

+ 0
- 0
lavender/migrations/__init__.py View File


+ 101
- 0
lavender/models.py View File

@@ -0,0 +1,101 @@
from functools import partial

from django.contrib.auth.models import AbstractUser
from django.contrib.auth.signals import user_logged_in
from django.db import models
from django.dispatch import receiver
from django.utils.timezone import now

from lavender import texture


class Player(AbstractUser):
packages = models.ManyToManyField('Package', blank=True)


class LogHistory(models.Model):
player: Player = models.ForeignKey(Player, on_delete=models.CASCADE)

date = models.DateTimeField(default=now)
source = models.CharField(max_length=255)

def __str__(self):
return f"{self.player.username} at {self.date} from {self.source}"


class Token(models.Model):
player: Player = models.ForeignKey(Player, on_delete=models.CASCADE)

access = models.CharField(max_length=32, null=True)
client = models.CharField(max_length=32, null=True)
uuid = models.CharField(max_length=32, null=True)
server_id = models.CharField(max_length=48, null=True)
created = models.DateTimeField(default=now)

def __repr__(self):
return f"{self.player.username} (access: {self.access} client: {self.client}"


class GameLog(models.Model):
player: Player = models.ForeignKey(Player, on_delete=models.CASCADE)

date = models.DateTimeField(default=now)
kind = models.CharField(max_length=16)
payload = models.TextField(default="")

def __str__(self):
return f"{self.player.username} at {self.date}: {self.kind}"


class Package(models.Model):
name = models.CharField(max_length=16)
version = models.CharField(max_length=16)
title = models.CharField(max_length=255)
location = models.CharField(max_length=255)
is_default = models.BooleanField(default=False)

def __str__(self):
return self.name


class Quenta(models.Model):
player: Player = models.OneToOneField(Player, on_delete=models.CASCADE, null=True)

text = models.TextField()
comments = models.TextField(blank=True)
approved = models.BooleanField(default=False)

def __str__(self):
status = "approved" if self.approved else "declined"
return f"{self.__class__.__name__} for {self.player.username}: {status}"


class Wardrobe(models.Model):
player: Player = models.ForeignKey(Player, on_delete=models.CASCADE)

skin_height = models.PositiveIntegerField(null=True)
skin_width = models.PositiveIntegerField(null=True)
skin = models.ImageField(null=True, blank=True, height_field='skin_height', width_field='skin_width',
upload_to=partial(texture.path_handler, texture.TextureType.SKIN),
storage=texture.OverwriteStorage())

cloak_height = models.PositiveIntegerField(null=True)
cloak_width = models.PositiveIntegerField(null=True)
cloak = models.ImageField(null=True, blank=True, height_field='cloak_height', width_field='cloak_width',
upload_to=partial(texture.path_handler, texture.TextureType.CLOAK),
storage=texture.OverwriteStorage())

elytra_height = models.PositiveIntegerField(null=True)
elytra_width = models.PositiveIntegerField(null=True)
elytra = models.ImageField(null=True, blank=True, height_field='elytra_height', width_field='elytra_width',
upload_to=partial(texture.path_handler, texture.TextureType.ELYTRA),
storage=texture.OverwriteStorage())

def __str__(self):
return f"{self.__class__.__name__} for {self.player.username}"


@receiver(user_logged_in)
def on_login(sender, user, request, **kwargs):
record = LogHistory(player=user, source='site')
record.save()

+ 6
- 0
lavender/settings.py View File

@@ -0,0 +1,6 @@
from django.conf import settings

AUTH_API_ENABLED = getattr(settings, 'LAVENDER_AUTH_API_ENABLED', False)
LEGACY_AUTH_API_ENABLED = getattr(settings, 'LAVENDER_LEGACY_AUTH_API_ENABLED', False)
LEGACY_AUTH_FAIL_MESSAGE = getattr(settings, 'LAVENDER_LEGACY_AUTH_FAIL_MESSAGE', 'Failed to login')
AUTH_POSTPROCESS_CLASS = getattr(settings, 'LAVENDER_AUTH_POSTPROCESS_CLASS', None)

+ 22
- 0
lavender/texture.py View File

@@ -0,0 +1,22 @@
from enum import Enum
from pathlib import Path

from django.conf import settings
from django.core.files.storage import FileSystemStorage


class TextureType(Enum):
SKIN = "skin"
CLOAK = "cloak"
ELYTRA = "elytra"


class OverwriteStorage(FileSystemStorage):
def get_available_name(self, name, max_length=None):
if self.exists(name):
Path(settings.MEDIA_ROOT, name).unlink()
return name


def path_handler(texture_type: TextureType, instance, filename: str) -> str:
return f'{texture_type.value}s/{instance.player.username}.png'

+ 42
- 0
lavender/urls.py View File

@@ -0,0 +1,42 @@
import debug_toolbar
from django.conf import settings
from django.conf.urls.static import static
from django.contrib.auth import views as auth
from django.contrib import admin
from django.urls import path, include

from lavender.views import registration, profile
from lavender.views.api import authentication, packages, telemetry

urlpatterns = [
path('', auth.LoginView.as_view(redirect_authenticated_user=True)),

path('admin/', admin.site.urls),
path('accounts/login/', auth.LoginView.as_view(redirect_authenticated_user=True), name='login'),
path('accounts/logout/', auth.LogoutView.as_view(), name='logout'),

path('accounts/password_change/', auth.PasswordChangeView.as_view(), name='password_change'),
path('accounts/password_change/done/', auth.PasswordChangeDoneView.as_view(), name='password_change_done'),

path('accounts/password_reset/', auth.PasswordResetView.as_view(), name='password_reset'),
path('accounts/password_reset/done/', auth.PasswordResetDoneView.as_view(), name='password_reset_done'),
path('accounts/reset/<uidb64>/<token>/', auth.PasswordResetConfirmView.as_view(), name='password_reset_confirm'),
path('accounts/reset/done/', auth.PasswordResetCompleteView.as_view(), name='password_reset_complete'),

path('accounts/registration/', registration.registration, name='registration'),
path('accounts/profile/', profile.profile, name='profile'),

path('api/v1/authentication/new/<username>/<password>/', authentication.auth),
path('api/v1/authentication/legacy/<username>/<password>/', authentication.auth_legacy),
path('api/v1/logs/upload/', telemetry.save_logs),
path('api/v1/packages/list/', packages.package_list),

# FIXME: Deprecated and will be removed soon
path('api/auth/<username>/<password>/', authentication.auth),
path('api/auth_legacy/<username>/<password>/', authentication.auth_legacy),
path('api/packages/', packages.package_list),
]

if settings.DEBUG:
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
urlpatterns += [path('__debug__/', include(debug_toolbar.urls))]

+ 36
- 0
lavender/utils.py View File

@@ -0,0 +1,36 @@
import re

INVALID_CHARS = re.compile(r'[^a-zA-Z_]')
REPEATS = re.compile(r'(?P<char>.+)(?P=char){2,}', re.IGNORECASE)
SIDE_DASHES = re.compile(r'^[_-]|[_-]$')


def check_nickname(nickname: str):
# В идеале этого не должно происходить, но пусть будет
if len(nickname) == 0:
return ['Никнейм не может быть пустым']

result = []

if len(nickname) < 3 or len(nickname) > 16:
result.append('Ник должен быть длиной от 3 до 16 символов')

if INVALID_CHARS.search(nickname):
result.append((
'Ник может состоять только из английских '
'букв или нижнего подчёркивания'
))

if REPEATS.search(nickname):
result.append((
'Ник не должен содержать '
'повторов из трёх и более символов'
))

if re.search(SIDE_DASHES, nickname):
result.append((
'Ник не должен содержать подчёркиваний, '
'дефисов и прочих украшений по краям'
))

return result

+ 0
- 0
lavender/views/__init__.py View File


+ 0
- 0
lavender/views/api/__init__.py View File


+ 56
- 0
lavender/views/api/authentication.py View File

@@ -0,0 +1,56 @@
from importlib import import_module
from typing import Optional

from django.contrib.auth import authenticate
from django.http import HttpResponse, Http404

from lavender import settings
from lavender.models import LogHistory, Player


def _postprocess(user: Optional[Player]) -> bool:
if user is None:
return False

if settings.AUTH_POSTPROCESS_CLASS is None:
return True

module_name, class_name = settings.AUTH_POSTPROCESS_CLASS.rsplit(".", 1)
module = import_module(module_name)
assert hasattr(module, class_name), f"Class {class_name} is not in {module_name}"
checker = getattr(module, class_name)()
return checker.check(user)


def _auth(username, password, handler):
user = authenticate(username=username, password=password)
if user:
record = LogHistory(player=user, source='game')
record.save()

return handler(_postprocess(user))


def auth(request, username, password):
if not settings.AUTH_API_ENABLED:
raise Http404

def handler(success: bool) -> HttpResponse:
status: int = success and 204 or 403
return HttpResponse(status=status)

return _auth(username, password, handler)


def auth_legacy(request, username, password):
if not settings.LEGACY_AUTH_API_ENABLED:
raise Http404

def handler(success: bool) -> HttpResponse:
if success:
message = f"OK:{username}"
else:
message = settings.LEGACY_AUTH_FAIL_MESSAGE
return HttpResponse(message)

return _auth(username, password, handler)

+ 39
- 0
lavender/views/api/decorator.py View File

@@ -0,0 +1,39 @@
from functools import wraps
from typing import Callable, Any

from django.http import HttpRequest, HttpResponseForbidden, HttpResponse
from pydantic import BaseModel

from lavender.models import Player

access_header = "X-Lavender-Access"
client_header = "X-Lavender-Client"

Decorator = Callable[[HttpRequest], HttpResponse]


def authenticated(func: Callable[[Player, HttpRequest], BaseModel]):
@wraps(func)
def wrapper(request: HttpRequest):
access_token = request.headers[access_header]
client_token = request.headers[client_header]
if not all((access_header, client_header)):
raise HttpResponseForbidden

player: Player = Player.objects.get(token__access=access_token, token__client=client_token)
if player is None:
raise HttpResponseForbidden

response = func(player, request)
return response

return wrapper


def json_response(func: Callable[[HttpRequest], BaseModel]) -> Decorator:
@wraps(func)
def wrapper(request: HttpRequest):
result = func(request)
return HttpResponse(result.json(), status=200, content_type='application/json')

return wrapper

+ 38
- 0
lavender/views/api/packages.py View File

@@ -0,0 +1,38 @@
from typing import List

from django.views.decorators.csrf import csrf_exempt
from pydantic import BaseModel

from lavender.models import Package, Player
from lavender.views.api.decorator import authenticated, json_response


class PackageInstance(BaseModel):
name: str
title: str
version: str
location: str

class Config:
allow_mutation = False
orm_mode = True


class PackageList(BaseModel):
minimumVersion: int = 3
packages: List[PackageInstance] = []

class Config:
allow_mutation = False


@csrf_exempt
@json_response
@authenticated
def package_list(player: Player, _) -> PackageList:
packages = [
PackageInstance.from_orm(package)
for package in
Package.objects.filter(player=player)
]
return PackageList(packages=packages)

+ 27
- 0
lavender/views/api/telemetry.py View File

@@ -0,0 +1,27 @@
import json
from typing import List

from django.http import HttpRequest, HttpResponse
from django.views.decorators.csrf import csrf_exempt
from pydantic import BaseModel, parse_obj_as

from lavender.models import Player, GameLog
from lavender.views.api.decorator import authenticated


class LogInstance(BaseModel):
kind: str
payload: str


@csrf_exempt
@authenticated
def save_logs(player: Player, request: HttpRequest):
log_instances: List[LogInstance] = parse_obj_as(List[LogInstance], json.loads(request.body))
logs = [
GameLog(player=player, kind=log.kind, payload=log.payload)
for log
in log_instances
]
GameLog.objects.bulk_create(logs)
return HttpResponse()

+ 70
- 0
lavender/views/profile.py View File

@@ -0,0 +1,70 @@
from lavender.forms import EmailChangeForm, QuentaForm, WardrobeForm
from django.contrib import messages
from django.contrib.auth.decorators import login_required
from django.contrib.auth.forms import PasswordChangeForm
from django.shortcuts import render

from lavender.models import Quenta, Wardrobe


@login_required
def profile(request):
email_form = EmailChangeForm(instance=request.user)
password_form = PasswordChangeForm(user=request.user)
quenta = Quenta.objects.filter(player=request.user).first()
wardrobe = Wardrobe.objects.filter(player=request.user).first()
quenta_form = QuentaForm(user=request.user, instance=quenta)
wardrobe_form = WardrobeForm(user=request.user, instance=wardrobe)

if request.method == 'POST':
if request.POST['form'] == 'email':
email_form = EmailChangeForm(request.POST, instance=request.user)
if email_form.is_valid():
email_form.save()
messages.success(request, 'Почта успешно изменена!')
else:
for field in email_form:
for error in field.errors:
messages.error(request, error)

elif request.POST['form'] == 'password':
password_form = PasswordChangeForm(data=request.POST, user=request.user)
if password_form.is_valid():
password_form.save()
messages.success(request, 'Пароль успешно изменён!')
else:
for field in password_form:
for error in field.errors:
messages.error(request, error)

elif request.POST['form'] == 'quenta':
quenta_form = QuentaForm(data=request.POST, user=request.user, instance=quenta)
if quenta_form.is_valid():
quenta_form.save()
messages.success(request, 'Квента успешно сохранена!')
else:
for field in quenta_form:
for error in field.errors:
messages.error(request, error)

elif request.POST['form'] == 'wardrobe':
wardrobe_form = WardrobeForm(request.POST, request.FILES, user=request.user, instance=wardrobe)
if wardrobe_form.is_valid():
wardrobe_form.save()
wardrobe = Wardrobe.objects.filter(player=request.user).first()
messages.success(request, 'Скин успешно сохранен!')
else:
for field in quenta_form:
for error in field.errors:
messages.error(request, error)

return render(request, 'profile/profile.html', {
'user': request.user,
'history': request.user.loghistory_set.order_by('-id')[:10],
'quenta': quenta,
'wardrobe': wardrobe,
'email_form': email_form,
'password_form': password_form,
'quenta_form': quenta_form,
'wardrobe_form': wardrobe_form,
})

+ 29
- 0
lavender/views/registration.py View File

@@ -0,0 +1,29 @@
from lavender.forms import RegistrationForm
from django.contrib.auth import authenticate, login
from django.http import HttpResponseRedirect
from django.shortcuts import redirect, render

from lavender.models import Package


def registration(request):
if request.user.is_authenticated:
return HttpResponseRedirect('/accounts/profile')

if request.method == 'POST':
form = RegistrationForm(request.POST)
if form.is_valid():
form.save()

username = form.cleaned_data['username']
raw_password = form.cleaned_data['password1']

user = authenticate(username=username, password=raw_password)
user.packages.add(*Package.objects.filter(is_default=True))
user.save()

login(request, user)
return redirect('/accounts/profile')
else:
form = RegistrationForm()
return render(request, 'registration/registration.html', {'form': form})

+ 21
- 0
manage.py View File

@@ -0,0 +1,21 @@
#!/usr/bin/env python3
"""Django's command-line utility for administrative tasks."""
import os
import sys


def main():
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'lavender.settings')
try:
from django.core.management import execute_from_command_line
except ImportError as exc:
raise ImportError(
"Couldn't import Django. Are you sure it's installed and "
"available on your PYTHONPATH environment variable? Did you "
"forget to activate a virtual environment?"
) from exc
execute_from_command_line(sys.argv)


if __name__ == '__main__':
main()

+ 26
- 0
pylintrc View File

@@ -0,0 +1,26 @@
[MASTER]
unsafe-load-any-extension=y

[MESSAGES CONTROL]
# W0142 -- Used * or ** magic
# W0212 -- Access to a protected member
# W0603 -- Using the global statement
# W0613 -- Unused argument
# W0702 -- No exception type(s) specified
# W0703 -- Catching too general exception Exception
# C0103 -- Invalid variable name
# C0111 -- Missing docstring
# C0301 -- Line too long
# C0322 -- Wrong continued indentation
# C0330 -- Wrong hanging indentation
# R0201 -- Method could be a function
# R0801 -- Similar lines
# R0901 -- Too many ancestors
# R0903 -- Too few public methods
# R0904 -- Too many public methods
# R0912 -- Too many branches
# R0913 -- Too many arguments
# R0914 -- Too many local variables
# R0204 -- Redefinition of variable type
# I0011 -- Locally disabling
disable=W0142,W0212,W0603,W0613,W0702,W0703,C0103,C0111,C0301,C0322,C0330,R0201,R0801,R0901,R0903,R0904,R0912,R0913,R0914,R0204,I0011

+ 6
- 0
requirements.txt View File

@@ -0,0 +1,6 @@
Django==2.2.1,<3
mysqlclient==1.4.6
django-debug-toolbar==2.1
Pillow==6.2.1
singleton-decorator==1.0.0
pydantic>=1.3

+ 11
- 0
requirements_dev.txt View File

@@ -0,0 +1,11 @@
wheel>=0.33
pylint>=2.4
pytest>=3,<4
pytest-cov>=2.8
pytest-runner>=4,<5
twine>=1.12
coverage>=4.5,<5
flake8>=3.7
tox>=3.14
Sphinx>=1.8
watchdog>=0.9

+ 33
- 0
setup.cfg View File

@@ -0,0 +1,33 @@
[bdist_wheel]
universal = 1

[flake8]
exclude = docs
max-line-length = 120

[aliases]
# Define setup.py command aliases here
test = pytest

[tool:pytest]
collect_ignore =
setup.py
norecursedirs =
.git
.tox
dist
build
python_files =
test_*.py
*_test.py
tests.py
addopts =
-v
; --strict
; --ignore docs/conf.py
; --ignore setup.py
; --doctest-modules
; --doctest-glob \*.rst
; --tb short
; --cov-report term-missing


+ 47
- 0
setup.py View File

@@ -0,0 +1,47 @@
from pathlib import Path
from setuptools import setup, find_packages

readme = Path('README.md').read_text()
history = Path('HISTORY.md').read_text()

requirements = Path('requirements.txt').read_text().splitlines()
setup_requirements = ['wheel']
test_requirements = Path('requirements_dev.txt').read_text().splitlines()

setup(
name="mc4ep_lavender",
version="0.8.0",
license="MIT",
description="MC4EP cabinet core",
long_description=readme + '\n\n' + history,
long_description_content_type='text/markdown',
author="Andriy Kushnir (Orhideous)",
author_email="[email protected]",
url="https://git.mc4ep.org/mc4ep/lavender",
packages=find_packages(),
include_package_data=True,
zip_safe=False,
classifiers=[
"Development Status :: 4 - Beta",
"Environment :: No Input/Output (Daemon)",
"Intended Audience :: Developers",
"Intended Audience :: System Administrators",
"License :: OSI Approved",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
"Programming Language :: Python",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.6",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: Implementation :: CPython",
"Programming Language :: Python :: Implementation :: PyPy",
"Topic :: Internet :: WWW/HTTP :: WSGI",
"Topic :: Internet :: WWW/HTTP :: WSGI :: Application",
"Topic :: Utilities",
],
keywords=["Minecraft", "Yggdrasil", "Authentication", "Nidhoggr", "Lavender"],
setup_requires=setup_requirements,
install_requires=requirements,
tests_require=test_requirements,
test_suite='tests',
)

+ 0
- 0
tests/__init__.py View File


Loading…
Cancel
Save