diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 0000000..105ce2d --- /dev/null +++ b/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..090d630 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..33d285e --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/monsterforge.iml b/.idea/monsterforge.iml new file mode 100644 index 0000000..f8a72f6 --- /dev/null +++ b/.idea/monsterforge.iml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 57a59d7..ed78ddb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,16 @@ # Changelog + +## 1.2.0 (2021-09-14) + +### Improvements + +* Code refactoring and cleanup +* Migrate to Django 3.2 (From Django 2.2) +* Enhance RPG Agnostic aspect +* Several bugfixes +* Dependency updates + ## 1.1.1 (2021-08-19) ### Improvements diff --git a/README.md b/README.md index 8f3c793..ef3dfba 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,8 @@ The website at [forge.dice.quest](https://forge.dice.quest) provides tools for t The website is free and contains no ads. Feel free to contribute anything from commenting to design or new options and tools. If you plan something bigger, please contact me beforehand. +This project is supported by [JetBrains - PyCharm](https://www.jetbrains.com/pycharm/)! + ### Fork I took over as maintainer for the project originally made by matnad. I have since made it more perfomand, slimmer as well as added new features to it. diff --git a/dndtools/settings.py b/dndtools/settings.py index 8666f7a..4085654 100644 --- a/dndtools/settings.py +++ b/dndtools/settings.py @@ -9,13 +9,11 @@ For the full list of settings and their values, see https://docs.djangoproject.com/en/2.0/ref/settings/ """ - import os # import secret settings from .settings_secret import * - # Build paths inside the project like this: os.path.join(BASE_DIR, ...) BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) @@ -32,7 +30,7 @@ else: DEBUG = True -ALLOWED_HOSTS = ['*'] +ALLOWED_HOSTS = ['*'] # Email @@ -52,12 +50,11 @@ 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', - 'django.contrib.sites', + 'django.contrib.sites', 'paperminis.apps.PaperminisConfig', 'widget_tweaks', ] - MIDDLEWARE = [ 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', @@ -73,8 +70,8 @@ TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [os.path.join(BASE_DIR, 'templates'), os.path.join(BASE_DIR, 'templates/paperminis'), os.path.join(BASE_DIR, 'templates/registration')] - , + 'DIRS': [os.path.join(BASE_DIR, 'templates'), os.path.join(BASE_DIR, 'templates/paperminis'), + os.path.join(BASE_DIR, 'templates/registration')], 'APP_DIRS': True, 'OPTIONS': { 'context_processors': [ @@ -89,7 +86,6 @@ WSGI_APPLICATION = 'dndtools.wsgi.application' - # Database # https://docs.djangoproject.com/en/2.0/ref/settings/#databases if os.getenv('PRODUCTION', False) == "True": @@ -111,7 +107,6 @@ } } - # Password validation # https://docs.djangoproject.com/en/2.0/ref/settings/#auth-password-validators @@ -130,7 +125,6 @@ }, ] - # Internationalization # https://docs.djangoproject.com/en/2.0/topics/i18n/ @@ -144,7 +138,6 @@ USE_TZ = True - LOGGING = { 'version': 1, 'disable_existing_loggers': False, @@ -178,3 +171,6 @@ # Due to BestiaryLink we need to set this value higher DATA_UPLOAD_MAX_NUMBER_FIELDS = 2500 + +# Set the AUTO_FIELD globally for Django 3.2 +DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' diff --git a/example_monster.json b/example_monster.json new file mode 100644 index 0000000..824bd67 --- /dev/null +++ b/example_monster.json @@ -0,0 +1,16 @@ +{ + "RPG Agnostic Creature": { + "img_url": "https://www.dndbeyond.com/avatars/thumbnails/0/301/1000/1000/636252771691385727.jpeg", + "name": "Orc", + "creature_size": "Medium" + }, + "A Dnd SRE Creature": { + "CR": "3", + "alignment": "chaotic evil", + "img_url": "https://www.dndbeyond.com/avatars/thumbnails/0/74/1000/1000/636252734224239957.jpeg", + "name": "Werewolf", + "creature_size": "Medium", + "source": "Basic Rules", + "creature_type": "Humanoid" + } +} \ No newline at end of file diff --git a/paperminis/__init__.py b/paperminis/__init__.py index 5b96b20..af59e65 100644 --- a/paperminis/__init__.py +++ b/paperminis/__init__.py @@ -1,2 +1,2 @@ -__version__ = '1.1.1' +__version__ = '1.2.0' VERSION = __version__ # synonym diff --git a/paperminis/admin.py b/paperminis/admin.py index 7be8f52..d12f29b 100644 --- a/paperminis/admin.py +++ b/paperminis/admin.py @@ -26,6 +26,7 @@ class UserAdmin(DjangoUserAdmin): search_fields = ('email', 'first_name', 'last_name') ordering = ('email',) + class BestiaryInline(admin.TabularInline): model = CreatureQuantity extra = 1 @@ -35,7 +36,6 @@ def get_formset(self, request, obj=None, **kwargs): self.parent_obj = obj return super(BestiaryInline, self).get_formset(request, obj, **kwargs) - def formfield_for_foreignkey(self, db_field, request, **kwargs): parent = self.parent_obj if db_field.name == "creature": @@ -49,12 +49,14 @@ class CreatureAdmin(admin.ModelAdmin): list_filter = ['size'] fields = ['owner', 'name', 'img_url', 'size'] + @admin.register(Bestiary) class CreatureAdmin(admin.ModelAdmin): list_display = ('owner', 'id', 'name') fields = ['owner', 'name'] inlines = [BestiaryInline] + @admin.register(CreatureQuantity) class CreatureQuantityAdmin(admin.ModelAdmin): list_display = ('owner', 'id', 'creature', 'bestiary') diff --git a/paperminis/forms.py b/paperminis/forms.py index 8c0858d..f6ab3cb 100644 --- a/paperminis/forms.py +++ b/paperminis/forms.py @@ -12,8 +12,10 @@ from django.contrib.auth.forms import UserCreationForm from django.contrib.auth import get_user_model + User = get_user_model() + class SignUpForm(UserCreationForm): """Used to register new users""" # email @@ -21,21 +23,24 @@ class SignUpForm(UserCreationForm): class Meta: model = User - fields = ('email', 'password1', 'password2', ) + fields = ('email', 'password1', 'password2',) class CreatureModifyForm(forms.ModelForm): """Add or Update creatures""" name = forms.CharField(widget=forms.TextInput(attrs={'class': 'form-control', 'size': '60%'})) + class Meta: model = Creature fields = ['name', 'show_name', 'img_url', 'creature_type', 'size', 'CR', 'position', 'color'] + def __init__(self, *args, **kwargs): self.user = kwargs.pop('user') # To get request.user. Do not use kwargs.pop('user', None) due to potential security hole - #self.method = kwargs.pop('method') # To get create or update + # self.method = kwargs.pop('method') # To get create or update super(CreatureModifyForm, self).__init__(*args, **kwargs) self.fields['color'].required = False self.fields['position'].required = False + def clean(self): cleaned_data = super(CreatureModifyForm, self).clean() if self.user: @@ -43,9 +48,11 @@ def clean(self): img_url = cleaned_data.get("img_url") cr = cleaned_data.get("CR") # name + img_url must be unique. Otherwise raise a ValidationError - count = Creature.objects.filter(owner=self.user,name=name,img_url=img_url).exclude(id=self.instance.id).count() + count = Creature.objects.filter(owner=self.user, name=name, img_url=img_url).exclude( + id=self.instance.id).count() if count == 1: - raise forms.ValidationError(('This creature already exists. Use a different name or image url.'), code='exists',) + raise forms.ValidationError(('This creature already exists. Use a different name or image url.'), + code='exists', ) # patreon early access backend validation # if self.user.groups.filter(name='Patrons').count() <= 0: @@ -56,16 +63,18 @@ def clean(self): if not isinstance(cr, float) or cr < 0 or cr > 1000: raise forms.ValidationError(('CR must a number be between 0 and 1000.'), code='value error', ) - return cleaned_data + class BestiaryModifyForm(forms.ModelForm): """Simple bestiary create/update form""" name = forms.CharField(widget=forms.TextInput(attrs={'class': 'form-control', 'size': '80%'})) + class Meta: model = Bestiary fields = ['name', ] + def validate_ddbenc_url(value): if not value: return # Required error is done the field @@ -77,20 +86,23 @@ def validate_ddbenc_url(value): class DDBEncounterBestiaryCreate(forms.Form): """create bestiary and monsters from ddb encounter""" # URL - ddb_enc_url = forms.URLField(help_text="Required. Enter a Dndbeyond Encounter URL.",validators=[validate_ddbenc_url]) + ddb_enc_url = forms.URLField(help_text="Required. Enter a Dndbeyond Encounter URL.", + validators=[validate_ddbenc_url]) class QuantityForm(forms.ModelForm): """Form to link creatures to bestiary. Most of this is done in views. """ quantity = forms.IntegerField() - #name = forms.CharField(max_length=150) + + # name = forms.CharField(max_length=150) class Meta: model = Creature - fields = ['name',] + fields = ['name', ] class PrintForm(forms.ModelForm): """Simple Form for print settings.""" + class Meta: model = PrintSettings exclude = ['user'] @@ -104,6 +116,7 @@ def clean_darken(self): raise forms.ValidationError("Enter a multiple of 1") return darken + def validate_file_extension(value): """Function to validate json file extension and size.""" ext = os.path.splitext(value.name)[1] @@ -111,7 +124,9 @@ def validate_file_extension(value): if not ext.lower() in valid_extensions: raise ValidationError('Unsupported file extension. Please only upload .json files.') if value.size > settings.MAX_UPLOAD_SIZE: - raise forms.ValidationError(_('Please keep filesize under %s. Current filesize %s') % (filesizeformat(settings.MAX_UPLOAD_SIZE), filesizeformat(value._size))) + raise forms.ValidationError(_('Please keep filesize under %s. Current filesize %s') % ( + filesizeformat(settings.MAX_UPLOAD_SIZE), filesizeformat(value._size))) + class UploadFileForm(forms.Form): """Upload form with validator.""" diff --git a/paperminis/generate_minis.py b/paperminis/generate_minis.py index 23bfc7c..f7ea1f2 100644 --- a/paperminis/generate_minis.py +++ b/paperminis/generate_minis.py @@ -1,50 +1,62 @@ import io import logging -import os import re from collections import Counter -from tempfile import TemporaryDirectory -from urllib.request import Request, urlopen from zipfile import ZIP_DEFLATED, ZipFile import cv2 as cv import numpy as np -from django.conf import settings -from greedypacker import BinManager from PIL import Image, ImageDraw, ImageFont +from greedypacker import BinManager -from paperminis.models import Bestiary, Creature, CreatureQuantity +from paperminis.models import Creature, CreatureQuantity from paperminis.utils import download_image - from .items import Item logger = logging.getLogger("django") -class MiniBuilder(): + +class MiniBuilder: def __init__(self, user): # user self.user = user self.sanitize = re.compile('[^a-zA-Z0-9\(\)\_@]', re.UNICODE) # sanitize user input - self.clean_email = self.sanitize.sub('',self.user.email) + self.clean_email = self.sanitize.sub('', self.user.email) - #TODO Clear this var + # TODO Clear this var self.file_name_body = self.clean_email self.creatures = [] + self.creature_counter = None + self.minis = [] + self.sheets = None + self.zip_container = None + + # Settings Containers + self.print_margin = None + self.dpmm = None # not fully supported setting yet, leave at 10 + self.grid_size = None self.enumerate = False - + self.force_name = None + self.base_shape = None + self.fixed_height = False + self.darken = None + self.font = cv.FONT_HERSHEY_SIMPLEX + self.paper_format = None + self.canvas = None + # clear download cache for each run download_image.cache_clear() def add_bestiary(self, pk): creature_quantities = CreatureQuantity.objects.filter(owner=self.user, bestiary=pk) - bestiary_name = self.sanitize.sub('',creature_quantities.first().bestiary.name) + bestiary_name = self.sanitize.sub('', creature_quantities.first().bestiary.name) if self.file_name_body == self.clean_email: self.file_name_body = bestiary_name else: - self.file_name_body += '_'+bestiary_name + self.file_name_body += '_' + bestiary_name if creature_quantities: creatures = [] for cq in creature_quantities: @@ -63,10 +75,9 @@ def add_creatures(self, creatures): else: return False - def load_settings(self, paper_format='a4', - print_margin=np.array([3.5,4]), + print_margin=np.array([3.5, 4]), grid_size=24, base_shape='square', enumerate=False, @@ -75,28 +86,26 @@ def load_settings(self, darken=0): self.print_margin = print_margin - self.dpmm = 10 # not fully supported setting yet, leave at 10 + self.dpmm = 10 # not fully supported setting yet, leave at 10 self.grid_size = grid_size self.enumerate = enumerate self.force_name = force_name self.base_shape = base_shape self.fixed_height = fixed_height self.darken = darken - self.font = cv.FONT_HERSHEY_SIMPLEX self.paper_format = paper_format paper = {'a3': np.array([297, 420]), 'a4': np.array([210, 297]), 'letter': np.array([216, 279]), 'legal': np.array([216, 356]), 'tabloid': np.array([279, 432])} - self.canvas = (paper[paper_format] - 2 * print_margin) * self.dpmm - + self.canvas = (paper[paper_format] - 2 * self.print_margin) * self.dpmm def build_all_and_zip(self): if self.enumerate: # if enumerate is true, settings are always loaded self.creature_counter = Counter([c.name for c in self.creatures]) - self.creature_counter = {key:val for key, val in self.creature_counter.items() if val > 1} + self.creature_counter = {key: val for key, val in self.creature_counter.items() if val > 1} self.minis = [] for creature in self.creatures: @@ -107,16 +116,16 @@ def build_all_and_zip(self): print('{} skipped with error: {}'.format(creature.name, mini)) self.sheets = self.build_sheets(self.minis) - self.zip_path = self.save_and_zip(self.sheets) + self.zip_container = self.save_and_zip(self.sheets) logger.info(download_image.cache_info()) - return self.zip_path + return self.zip_container def build_mini(self, creature): - if not hasattr(self,'grid_size'): + if not hasattr(self, 'grid_size'): # check if settings loaded manually, otherwise load default settings self.load_settings() - if not isinstance(creature,Creature): + if not isinstance(creature, Creature): return 'Object is not a Creature.' if creature.img_url == '': @@ -127,11 +136,11 @@ def build_mini(self, creature): # I will keep them in for now min_height_mm = 40 if creature.size in ['S', 'T']: - m_width = int(self.grid_size/2) + m_width = int(self.grid_size / 2) max_height_mm = 30 n_height = 6 - font_size = 1.15 # opencv "height" - font_height = 40 # PIL drawing max height for n_height = 8 + font_size = 1.15 # opencv "height" + font_height = 40 # PIL drawing max height for n_height = 8 font_width = 1 enum_size = 1.2 enum_width = 3 @@ -139,8 +148,8 @@ def build_mini(self, creature): m_width = self.grid_size max_height_mm = 40 n_height = 8 - font_size = 1.15 # opencv "height" - font_height = 50 # PIL drawing max height for n_height = 8 + font_size = 1.15 # opencv "height" + font_height = 50 # PIL drawing max height for n_height = 8 font_width = 1 enum_size = 2.2 enum_width = 3 @@ -151,8 +160,8 @@ def build_mini(self, creature): font_size = 2 font_height = 70 font_width = 2 - enum_size = 5 * self.grid_size/24 - enum_width = 8 * self.grid_size/24 + enum_size = 5 * self.grid_size / 24 + enum_width = 8 * self.grid_size / 24 elif creature.size == 'H': m_width = self.grid_size * 3 max_height_mm = 60 if not self.paper_format == 'letter' else 51 @@ -243,7 +252,7 @@ def build_mini(self, creature): show_name = False else: show_name = creature.show_name - + if show_name: # PIL fix for utf-8 characters n_img_pil = Image.new("RGB", (width, name_height), (255, 255, 255)) @@ -251,7 +260,7 @@ def build_mini(self, creature): y_margin = 0 # find optimal font size while x_margin < 2 or y_margin < 10: - #print(font_height) + # print(font_height) unicode_font = ImageFont.truetype("paperminis/DejaVuSans.ttf", font_height) font_height = round(font_height - 2, 2) textsize = unicode_font.getsize(text) @@ -259,17 +268,15 @@ def build_mini(self, creature): x_margin = im_w - textsize[0] y_margin = im_h - textsize[1] # write text - textX = x_margin//2 - textY = y_margin//2 + textX = x_margin // 2 + textY = y_margin // 2 draw = ImageDraw.Draw(n_img_pil) - draw.text((textX, textY), text, font=unicode_font, fill=(0,0,0)) + draw.text((textX, textY), text, font=unicode_font, fill=(0, 0, 0)) n_img = np.array(n_img_pil) cv.rectangle(n_img, (0, 0), (n_img.shape[1] - 1, n_img.shape[0] - 1), (0, 0, 0), thickness=1) else: n_img = np.zeros((1, width, 3), np.uint8) - - ## mimiature image m_img = download_image(creature.img_url) @@ -298,7 +305,7 @@ def build_mini(self, creature): white_vert = np.zeros((m_img.shape[0], 1, 3), np.uint8) + 255 m_img = np.concatenate((white_vert, m_img, white_vert), axis=1) - if m_img.shape[0] > max_height- 2: + if m_img.shape[0] > max_height - 2: f = (max_height - 2) / m_img.shape[0] m_img = cv.resize(m_img, (0, 0), fx=f, fy=f) white_horiz = np.zeros((1, m_img.shape[1], 3), np.uint8) + 255 @@ -321,13 +328,13 @@ def build_mini(self, creature): m_img = np.concatenate((np.zeros((diff, m_img.shape[1], 3), np.uint8) + 255, m_img), axis=0) elif creature.position == Creature.HOVERING: m_img = np.concatenate((np.zeros((top, m_img.shape[1], 3), np.uint8) + 255, m_img, - np.zeros((bottom, m_img.shape[1], 3), np.uint8) + 255), axis=0) + np.zeros((bottom, m_img.shape[1], 3), np.uint8) + 255), axis=0) elif creature.position == Creature.FLYING: - m_img = np.concatenate((m_img,np.zeros((diff, m_img.shape[1], 3), np.uint8) + 255), axis=0) + m_img = np.concatenate((m_img, np.zeros((diff, m_img.shape[1], 3), np.uint8) + 255), axis=0) else: return 'Position setting is invalid. Chose Walking, Hovering or Flying.' - #draw border + # draw border cv.rectangle(m_img, (0, 0), (m_img.shape[1] - 1, m_img.shape[0] - 1), (0, 0, 0), thickness=1) ## flipped miniature image @@ -337,52 +344,52 @@ def build_mini(self, creature): hsv = cv.cvtColor(m_img_flipped, cv.COLOR_BGR2HSV) h, s, v = cv.split(hsv) # darkening factor between 0 and 1 - factor = max(min((1-self.darken/100),1),0) + factor = max(min((1 - self.darken / 100), 1), 0) v[v < 255] = v[v < 255] * (factor) final_hsv = cv.merge((h, s, v)) m_img_flipped = cv.cvtColor(final_hsv, cv.COLOR_HSV2BGR) - - ## base bgr_color = tuple(int(creature.color[i:i + 2], 16) for i in (4, 2, 0)) demi_base = base_height // 2 - if creature.size == 'G': feet_mod = 1 - else: feet_mod = 2 + if creature.size == 'G': + feet_mod = 1 + else: + feet_mod = 2 base_height = int(np.floor(demi_base * feet_mod)) b_img = np.zeros((base_height, width, 3), np.uint8) + 255 # fill base if self.base_shape == 'square': cv.rectangle(b_img, (0, 0), (b_img.shape[1] - 1, demi_base - 1), bgr_color, thickness=-1) - cv.rectangle(b_img, (0, 0), (b_img.shape[1] - 1, b_img.shape[0] - 1), (0,0,0), thickness=1) + cv.rectangle(b_img, (0, 0), (b_img.shape[1] - 1, b_img.shape[0] - 1), (0, 0, 0), thickness=1) elif self.base_shape == 'circle': cv.rectangle(b_img, (0, 0), (b_img.shape[1] - 1, demi_base - 1), bgr_color, thickness=-1) - cv.rectangle(b_img, (0, 0), (b_img.shape[1] - 1, b_img.shape[0] - 1), (0,0,0), thickness=1) - cv.ellipse(b_img, (width//2, 0), (width//2, width//2), 0, 0, 180, bgr_color, -1) - cv.ellipse(b_img, (width // 2, 0), (width // 2, width // 2), 0, 0, 180, (0,0,0), 2) + cv.rectangle(b_img, (0, 0), (b_img.shape[1] - 1, b_img.shape[0] - 1), (0, 0, 0), thickness=1) + cv.ellipse(b_img, (width // 2, 0), (width // 2, width // 2), 0, 0, 180, bgr_color, -1) + cv.ellipse(b_img, (width // 2, 0), (width // 2, width // 2), 0, 0, 180, (0, 0, 0), 2) if feet_mod >= 2: cv.ellipse(b_img, (width // 2, base_height), (width // 2, width // 2), 0, 180, 360, (0, 0, 0), 2) - cv.line(b_img, (0, base_height), (width, base_height), (0,0,0), 3) + cv.line(b_img, (0, base_height), (width, base_height), (0, 0, 0), 3) elif self.base_shape == 'hexagon': - half = width//2 - hexagon_bottom = np.array([(0,0),(width//4,half),(width//4*3,half),(width,0)], np.int32) - hexagon_top = np.array([(0,width), (width//4,half), (width//4*3,half), (width,width)],np.int32) - cv.fillConvexPoly(b_img,hexagon_bottom,bgr_color,1) + half = width // 2 + hexagon_bottom = np.array([(0, 0), (width // 4, half), (width // 4 * 3, half), (width, 0)], np.int32) + hexagon_top = np.array([(0, width), (width // 4, half), (width // 4 * 3, half), (width, width)], np.int32) + cv.fillConvexPoly(b_img, hexagon_bottom, bgr_color, 1) if feet_mod >= 2: - cv.polylines(b_img,[hexagon_top],True,(0,0,0),2) + cv.polylines(b_img, [hexagon_top], True, (0, 0, 0), 2) else: return 'Invalid base shape. Choose square, hexagon or circle.' # enumerate if self.enumerate and creature.name in self.creature_counter: - #print(creature.name, self.creature_counter[creature.name]) + # print(creature.name, self.creature_counter[creature.name]) text = str(self.creature_counter[creature.name]) textsize = cv.getTextSize(text, self.font, enum_size, enum_width)[0] x_margin = b_img.shape[1] - textsize[0] y_margin = b_img.shape[0] - textsize[1] # Number color - if creature.color == 'ffffff': + if creature.color == 'ffffff': enum_color = (0, 0, 0) else: enum_color = (255, 255, 255) @@ -397,7 +404,7 @@ def build_mini(self, creature): img = np.concatenate((m_img, n_img, b_img), axis=0) # m_img_flipped = np.flip(m_img, 0) - nb_flipped = np.rot90(np.concatenate((n_img,b_img), axis=0), 2) + nb_flipped = np.rot90(np.concatenate((n_img, b_img), axis=0), 2) img = np.concatenate((nb_flipped, m_img_flipped, img), axis=0) ## Save image (not needed; only for debug/dev) @@ -407,9 +414,9 @@ def build_mini(self, creature): return img - def build_sheets(self,minis): + def build_sheets(self, minis): M = BinManager(self.canvas[0], self.canvas[1], pack_algo='guillotine', heuristic='best_shortside', - wastemap=True, rotation=True) + wastemap=True, rotation=True) its = {} item_id = 0 for m in minis: @@ -426,7 +433,7 @@ def build_sheets(self,minis): for r in result: img = np.zeros((int(self.canvas[1]), int(self.canvas[0]), 3), np.uint8) + 255 for it in r.items: - #print(it) + # print(it) x = int(it.x) y = int(it.y) w = int(it.width) @@ -437,7 +444,7 @@ def build_sheets(self,minis): if w > h: # rotated m_img = np.rot90(m_img, axes=(1, 0)) shape = m_img.shape - #print('x',x,'y',y,'shape',m_img.shape) + # print('x',x,'y',y,'shape',m_img.shape) img[y:y + shape[0], x:x + shape[1], :] = m_img sheets.append(img) @@ -447,22 +454,19 @@ def show_sheets(self, sheets): sheet_nr = 1 for sheet in sheets: RGB_img = cv.cvtColor(sheet, cv.COLOR_BGR2RGB) - img_small = cv.resize(sheet, (0,0), fx=.4, fy=.4) - cv.imshow('Img',img_small) + img_small = cv.resize(sheet, (0, 0), fx=.4, fy=.4) + cv.imshow('Img', img_small) cv.waitKey(0) def save_and_zip(self, sheets): sheet_nr = 1 - temp_dir = TemporaryDirectory() - #zip_fn = temp_dir.name + "/forged.zip" zip_memory = io.BytesIO() zipfile = ZipFile(zip_memory, mode='a', compression=ZIP_DEFLATED) for sheet in sheets: img_buffer = io.BytesIO() - RGB_img = cv.cvtColor(sheet, cv.COLOR_BGR2RGB) - im_pil = Image.fromarray(RGB_img) - sheet_fn = temp_dir.name + '/sheet_' + str(sheet_nr) + '.png' + rgb_img = cv.cvtColor(sheet, cv.COLOR_BGR2RGB) + im_pil = Image.fromarray(rgb_img) im_pil.save(img_buffer, dpi=(25.4 * self.dpmm, 25.4 * self.dpmm), format='PNG') img_buffer.seek(0) zipfile.writestr('sheet_' + str(sheet_nr) + '.png', img_buffer.getbuffer()) diff --git a/paperminis/items.py b/paperminis/items.py index 41729a1..95b7925 100644 --- a/paperminis/items.py +++ b/paperminis/items.py @@ -3,10 +3,13 @@ This is originally from greedypacker + with an item_id added to it. A legacy requirement for monsterforge TODO: See if we can clean this up proper and use greedypacker as is """ + + class Item: """ Items class for rectangles inserted into sheets """ + def __init__(self, width, height, item_id, CornerPoint: tuple = (0, 0), rotation: bool = True) -> None: @@ -18,8 +21,10 @@ def __init__(self, width, height, item_id, self.rotated = False self.id = 0 self.item_id = item_id + def __repr__(self): return 'Item(width=%r, height=%r, x=%r, y=%r, id=%r)' % (self.width, self.height, self.x, self.y, self.item_id) + def rotate(self) -> None: self.width, self.height = self.height, self.width self.rotated = False if self.rotated == True else True diff --git a/paperminis/management/commands/stats.py b/paperminis/management/commands/stats.py index 16c3907..dc45b0e 100644 --- a/paperminis/management/commands/stats.py +++ b/paperminis/management/commands/stats.py @@ -1,32 +1,31 @@ from django.core.management.base import BaseCommand, CommandError from paperminis.models import User, Creature, Bestiary + class Command(BaseCommand): help = 'List a number of stats' def handle(self, *args, **options): - # Get all users - total_users = User.objects.count() - - # Get all temp Users - temp_users = User.objects.filter(groups__name='temp').count() - - # Get all monsters - total_creatures = Creature.objects.count() - ddb_creature = Creature.objects.filter(from_ddb=True).count() - # Get all bestiaries - total_bestiary = Bestiary.objects.count() - ddb_bestiary = Bestiary.objects.filter(from_ddb=True).count() + # Get all users + total_users = User.objects.count() + # Get all temp Users + temp_users = User.objects.filter(groups__name='temp').count() + # Get all monsters + total_creatures = Creature.objects.count() + ddb_creature = Creature.objects.filter(from_ddb=True).count() + # Get all bestiaries + total_bestiary = Bestiary.objects.count() + ddb_bestiary = Bestiary.objects.filter(from_ddb=True).count() - self.stdout.write(self.style.SUCCESS('USER STATS')) - self.stdout.write(self.style.SUCCESS('Signed up users: %s' % str(total_users - temp_users))) - self.stdout.write(self.style.SUCCESS('Temporary Users: %s' % str(temp_users))) - self.stdout.write(self.style.SUCCESS('Total: %i\n' % total_users)) - self.stdout.write(self.style.SUCCESS('BESTIARY STATS')) - self.stdout.write(self.style.SUCCESS('Total: %i' % total_bestiary)) - self.stdout.write(self.style.SUCCESS('From D&DBeyond: %i\n' % ddb_bestiary)) - self.stdout.write(self.style.SUCCESS('CREATURE STATS')) - self.stdout.write(self.style.SUCCESS('Total: %i' % total_creatures)) - self.stdout.write(self.style.SUCCESS('From D&DBeyond: %i\n' % ddb_creature)) + self.stdout.write(self.style.SUCCESS('USER STATS')) + self.stdout.write(self.style.SUCCESS('Signed up users: %s' % str(total_users - temp_users))) + self.stdout.write(self.style.SUCCESS('Temporary Users: %s' % str(temp_users))) + self.stdout.write(self.style.SUCCESS('Total: %i\n' % total_users)) + self.stdout.write(self.style.SUCCESS('BESTIARY STATS')) + self.stdout.write(self.style.SUCCESS('Total: %i' % total_bestiary)) + self.stdout.write(self.style.SUCCESS('From D&DBeyond: %i\n' % ddb_bestiary)) + self.stdout.write(self.style.SUCCESS('CREATURE STATS')) + self.stdout.write(self.style.SUCCESS('Total: %i' % total_creatures)) + self.stdout.write(self.style.SUCCESS('From D&DBeyond: %i\n' % ddb_creature)) diff --git a/paperminis/migrations/0019_auto_20210831_0857.py b/paperminis/migrations/0019_auto_20210831_0857.py new file mode 100644 index 0000000..8ae5bc1 --- /dev/null +++ b/paperminis/migrations/0019_auto_20210831_0857.py @@ -0,0 +1,43 @@ +# Generated by Django 3.2.6 on 2021-08-31 08:57 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('paperminis', '0018_auto_20210318_2032'), + ] + + operations = [ + migrations.AlterField( + model_name='bestiary', + name='id', + field=models.BigAutoField(primary_key=True, serialize=False), + ), + migrations.AlterField( + model_name='creature', + name='id', + field=models.BigAutoField(primary_key=True, serialize=False), + ), + migrations.AlterField( + model_name='creaturequantity', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + migrations.AlterField( + model_name='printsettings', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + migrations.AlterField( + model_name='user', + name='first_name', + field=models.CharField(blank=True, max_length=150, verbose_name='first name'), + ), + migrations.AlterField( + model_name='user', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + ] diff --git a/paperminis/models.py b/paperminis/models.py index 85d3605..4adec4b 100644 --- a/paperminis/models.py +++ b/paperminis/models.py @@ -7,7 +7,7 @@ # Create your models here. # New User without name -from django.contrib.auth.models import AbstractUser, BaseUserManager ## A new class is imported. ## +from django.contrib.auth.models import AbstractUser, BaseUserManager ## A new class is imported. ## from django.db import models from django.utils.translation import ugettext_lazy as _ @@ -55,7 +55,8 @@ class User(AbstractUser): USERNAME_FIELD = 'email' REQUIRED_FIELDS = [] - objects = UserManager() ## This is the new line in the User model. ## + objects = UserManager() ## This is the new line in the User model. ## + class Creature(models.Model): """Model for Creatures""" @@ -68,7 +69,7 @@ class Creature(models.Model): HUGE = 'H' GARGANTUAN = 'G' - CREATURE_SIZE_CHOICES =( + CREATURE_SIZE_CHOICES = ( (TINY, 'Tiny'), (SMALL, 'Small'), (MEDIUM, 'Medium'), @@ -151,7 +152,7 @@ class Creature(models.Model): ) # fields - id = models.AutoField(primary_key=True) + id = models.BigAutoField(primary_key=True) name = models.CharField(max_length=100) img_url = models.TextField(max_length=500) size = models.CharField(max_length=1, choices=CREATURE_SIZE_CHOICES, default=MEDIUM) @@ -163,7 +164,6 @@ class Creature(models.Model): CR = models.FloatField(default=0) from_ddb = models.BooleanField(default=False) - # Metadata class Meta: ordering = ['name'] @@ -178,22 +178,21 @@ def __str__(self): return self.name - class Bestiary(models.Model): """Bestiary Model""" # fields - id = models.AutoField(primary_key=True) + id = models.BigAutoField(primary_key=True) name = models.CharField(max_length=150) owner = models.ForeignKey(User, on_delete=models.CASCADE) from_ddb = models.BooleanField(default=False) creatures = models.ManyToManyField(Creature, through='CreatureQuantity', - help_text='A list of creatures belonging to this bestiary.') + help_text='A list of creatures belonging to this bestiary.') + class Meta: ordering = ['name'] verbose_name_plural = "Bestiaries" - def get_absolute_url(self): """Returns the url to access a particular instance of Bestiary.""" return reverse('bestiary-detail', args=[str(self.id)]) @@ -202,6 +201,7 @@ def __str__(self): """String for representing the Bestiary object (in Admin site etc.).""" return self.name + class CreatureQuantity(models.Model): """Model to link bestiary with a quantity of creatures. "owner" is technically not needed (since it should be the same as owner of the bestiary), but adds another layer of security.""" diff --git a/paperminis/settings.py b/paperminis/settings.py index 67dce10..fae340d 100644 --- a/paperminis/settings.py +++ b/paperminis/settings.py @@ -1,3 +1,3 @@ # Redirect to home URL after login (Default redirects to /accounts/profile/) LOGIN_REDIRECT_URL = '/' -LOGOUT_REDIRECT_URL = '/' \ No newline at end of file +LOGOUT_REDIRECT_URL = '/' diff --git a/paperminis/urls.py b/paperminis/urls.py index c6994ac..b3e7fb2 100644 --- a/paperminis/urls.py +++ b/paperminis/urls.py @@ -4,7 +4,7 @@ urlpatterns = [ path('', views.index, name='index'), - #path('creatures/', views.CreatureListView.as_view(), name='creatures'), + # path('creatures/', views.CreatureListView.as_view(), name='creatures'), path('creature/', views.CreatureDetailView.as_view(), name='creature-detail'), path('creatures/', views.CreatureByUserListView.as_view(), name='creatures'), path('bestiary/', views.BestiaryDetailView.as_view(), name='bestiary-detail'), @@ -30,7 +30,7 @@ path('bestiary///unlink/', views.bestiary_unlink, name='bestiary-unlink'), path('bestiary//unlink/', views.bestiary_unlink, name='bestiary-unlink'), path('bestiary//print/', views.bestiary_print, name='bestiary-print'), - #path('bestiary//serve/', views.bestiary_serve_file, name='bestiary-serve') + # path('bestiary//serve/', views.bestiary_serve_file, name='bestiary-serve') ] # patreon diff --git a/paperminis/utils.py b/paperminis/utils.py index 930c685..e9561bc 100644 --- a/paperminis/utils.py +++ b/paperminis/utils.py @@ -1,3 +1,5 @@ +import json +from fractions import Fraction from functools import lru_cache from urllib.request import Request, urlopen @@ -5,6 +7,8 @@ import numpy as np from fake_useragent import UserAgent +from paperminis.models import Creature + # Observe this, in case this breaks things. This cache gets cleared when Minibuilder() gets evoked. # This is here to stop spamming image sources @@ -21,3 +25,78 @@ def download_image(url): return m_img except: return 'Image could not be found or loaded.' + + +def handle_json(f, user): + """Load and process .json file. + This version will update creatures if the json has more/different information. + A creature is uniquely identified by the tuple (name, img_url). + The update is still kind of slow for large files, but I can't see a better way to do it currently.""" + + try: + data = json.loads(f['file'].read().decode('utf-8')) + except: + return -1 + + current = Creature.objects.filter(owner=user) + current_name_url = [(x.name, x.img_url) for x in current] + current_full = [(x.name, x.img_url, x.size, x.CR, x.creature_type) for x in current] + size_map = {v: k for k, v in dict(Creature.CREATURE_SIZE_CHOICES).items()} + creature_type_map = {v: k for k, v in dict(Creature.CREATURE_TYPE_CHOICES).items()} + skip = 0 + obj_list = [] + for k, i in data.items(): + # mandatory fields + try: + name = i['name'] + img_url = i['img_url'] + name_url = (name, img_url) + except: + skip += 1 + continue + + # fix illegal size (default to medium) + try: + short_size = size_map[i['creature_size']] + except: + short_size = Creature.MEDIUM + + # fix illegal types (default to undefined) + try: + short_type = creature_type_map[i['creature_type']] + except: + short_type = Creature.UNDEFINED + + # fix illegal CRs (default to 0) + try: + cr = float(Fraction(i['CR'])) + if cr < 0 or cr > 1000: cr = 0 + except: + cr = 0 + + # check if unique + if name_url in current_name_url: + full_tup = (name, img_url, short_size, cr, short_type) + if full_tup in current_full: + # excact duplicate + skip += 1 + continue + else: + # updated attributes + # this is kinda slow :( + Creature.objects.filter(owner=user, name=name, img_url=img_url).update(size=short_size, CR=cr, + creature_type=short_type) + current_full.append(full_tup) + continue + + current_name_url.append(name_url) + + # if everything is ok, generate the object and store it + obj = Creature(owner=user, name=i['name'], size=short_size, img_url=i['img_url'], CR=cr, + creature_type=short_type) + obj_list.append(obj) + + if len(obj_list) > 0: + # MUCH faster than one query per entry! + Creature.objects.bulk_create(obj_list) + return skip diff --git a/paperminis/views.py b/paperminis/views.py index 8bb5947..da5a57d 100644 --- a/paperminis/views.py +++ b/paperminis/views.py @@ -1,49 +1,41 @@ -import json import logging -import os import uuid -from fractions import Fraction from urllib.parse import urljoin, urlparse import requests -from django import forms, template +from django import template from django.contrib.auth import authenticate, login from django.contrib.auth.decorators import login_required -from django.contrib.auth.forms import UserCreationForm from django.contrib.auth.hashers import make_password from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.models import Group from django.core.exceptions import ValidationError -from django.db import connection from django.db.models import Sum -from django.forms.models import modelformset_factory -#from django.shortcuts import get_object_or_404 -from django.http import (FileResponse, Http404, HttpResponse, - HttpResponseRedirect) +# from django.shortcuts import get_object_or_404 +from django.http import (FileResponse, Http404, HttpResponseRedirect) from django.shortcuts import render -from django.template.defaultfilters import filesizeformat from django.urls import reverse, reverse_lazy -from django.utils.deconstruct import deconstructible -from django.utils.encoding import smart_str from django.views import generic from django.views.generic.edit import CreateView, DeleteView, UpdateView -from dndtools.settings import STATIC_URL -from .forms import (DDBEncounterBestiaryCreate, PrintForm, QuantityForm, - SignUpForm, UploadFileForm) -from .generate_minis import MiniBuilder -from .models import Bestiary, Creature, CreatureQuantity, PrintSettings, User +from paperminis.forms import (DDBEncounterBestiaryCreate, PrintForm, QuantityForm, + SignUpForm, UploadFileForm) +from paperminis.generate_minis import MiniBuilder +from paperminis.models import Bestiary, Creature, CreatureQuantity, PrintSettings, User +from paperminis.utils import handle_json register = template.Library() logger = logging.getLogger("django") + @register.filter(name='has_group') def has_group(user, group_name): """Function to check Patreon status in templates.""" - group = Group.objects.get(name=group_name) + group = Group.objects.get(name=group_name) return group in user.groups.all() + def signup(request): """To register new users.""" if request.method == 'POST': @@ -65,7 +57,7 @@ def temp_account(request): """Create temporary account.""" if request.method == 'POST': # generate random username and password - password = make_password(uuid.uuid4()) + password = make_password(str(uuid.uuid4())) email = uuid.uuid4() user = User(email=email, password=password) user.save() @@ -79,6 +71,7 @@ def temp_account(request): return reverse('signup') + @login_required() def convert_account(request): """Convert temporary account to full account.""" @@ -103,84 +96,13 @@ def convert_account(request): form = SignUpForm() return render(request, 'convert.html', {'form': form}) -def handle_json(f, user): - """Load and process .json file. - This version will update creatures if the json has more/different information. - A creature is uniquely identified by the tuple (name, img_url). - The update is still kind of slow for large files, but I can't see a better way to do it currently.""" - - try: - data = json.loads(f['file'].read().decode('utf-8')) - except: - return -1 - - current = Creature.objects.filter(owner=user) - current_name_url = [(x.name, x.img_url) for x in current] - current_full = [(x.name, x.img_url, x.size, x.CR, x.creature_type) for x in current] - size_map = {v: k for k, v in dict(Creature.CREATURE_SIZE_CHOICES).items()} - creature_type_map = {v: k for k, v in dict(Creature.CREATURE_TYPE_CHOICES).items()} - skip = 0 - obj_list = [] - for k,i in data.items(): - # mandatory fields - try: - name = i['name'] - img_url = i['img_url'] - name_url = (name,img_url) - except: - skip += 1 - continue - - # fix illegal size (default to medium) - try: - short_size = size_map[i['creature_size']] - except: - short_size = Creature.MEDIUM - - # fix illegal types (default to undefined) - try: - short_type = creature_type_map[i['creature_type']] - except: - short_type = Creature.UNDEFINED - - # fix illegal CRs (default to 0) - try: - cr = float(Fraction(i['CR'])) - if cr < 0 or cr > 1000: cr = 0 - except: - cr = 0 - - # check if unique - if name_url in current_name_url: - full_tup = (name, img_url, short_size, cr, short_type) - if full_tup in current_full: - # excact duplicate - skip += 1 - continue - else: - # updated attributes - # this is kinda slow :( - Creature.objects.filter(owner=user, name=name, img_url=img_url).update(size=short_size, CR=cr, creature_type=short_type) - current_full.append(full_tup) - continue - - current_name_url.append(name_url) - - # if everything is ok, generate the object and store it - obj = Creature(owner=user, name=i['name'], size=short_size, img_url=i['img_url'], CR=cr, creature_type=short_type) - obj_list.append(obj) - - if len(obj_list) > 0: - # MUCH faster than one query per entry! - Creature.objects.bulk_create(obj_list) - return skip @login_required() def json_upload(request): """Upload view and form handling.""" if request.method == 'POST': form = UploadFileForm(request.POST, request.FILES) - if form.is_valid(): # validation happens in forms.py + if form.is_valid(): # validation happens in forms.py result = int(handle_json(request.FILES, request.user)) if result == -1: request.session['upload_error'] = 'Invalid File' @@ -193,6 +115,7 @@ def json_upload(request): form = UploadFileForm() return render(request, 'json_form.html', {'form': form}) + def index(request): """View function for home page.""" @@ -211,7 +134,8 @@ def get_queryset(self): class CreatureByUserListView(LoginRequiredMixin, generic.ListView): """Generic creatures by user list view.""" model = Creature - #paginate_by = 2 # pagination doesn't work with the current filter system and is not needed anyways + + # paginate_by = 2 # pagination doesn't work with the current filter system and is not needed anyways def get_queryset(self): return Creature.objects.filter(owner=self.request.user).order_by('name') @@ -223,13 +147,16 @@ def get_context_data(self, **kwargs): context['success'] = self.request.session.pop('success', None) return context + class BestiaryDetailView(LoginRequiredMixin, generic.DetailView): """Generic bestiary detail view.""" model = Bestiary def get_queryset(self): # get sum of creatures in there - return Bestiary.objects.filter(owner=self.request.user).annotate(total_creatures=Sum('creaturequantity__quantity')) + return Bestiary.objects.filter(owner=self.request.user).annotate( + total_creatures=Sum('creaturequantity__quantity')) + @login_required() def bestiary_print(request, pk): @@ -238,7 +165,7 @@ def bestiary_print(request, pk): if not bestiary: return HttpResponseRedirect(reverse('bestiaries')) elif bestiary.creatures.count() <= 0: - return HttpResponseRedirect(reverse('bestiary-detail', kwargs={'pk':pk})) + return HttpResponseRedirect(reverse('bestiary-detail', kwargs={'pk': pk})) # load print settings print_settings = PrintSettings.objects.filter(user=request.user) @@ -271,12 +198,12 @@ def bestiary_print(request, pk): print_settings.save() # load settings into the mini builder minis.load_settings(paper_format=print_settings.paper_format, - grid_size=print_settings.grid_size, - base_shape=print_settings.base_shape, - enumerate=print_settings.enumerate, - force_name=print_settings.force_name, - fixed_height = print_settings.base_shape, - darken= print_settings.darken) + grid_size=print_settings.grid_size, + base_shape=print_settings.base_shape, + enumerate=print_settings.enumerate, + force_name=print_settings.force_name, + fixed_height=print_settings.base_shape, + darken=print_settings.darken) # load creatures into the mini builder minis.add_bestiary(pk) # build minis @@ -286,14 +213,14 @@ def bestiary_print(request, pk): logger.info("Finished building, serving now.") # Clean bestiary name - name = minis.sanitize.sub('',bestiary.name) + name = minis.sanitize.sub('', bestiary.name) return FileResponse(archive, as_attachment=True, filename=name + "_forged.zip") - # seed form with loaded settings form = PrintForm(initial=print_settings.__dict__) - return render(request, 'bestiary_print.html', {'form':form}) + return render(request, 'bestiary_print.html', {'form': form}) + @login_required() def bestiary_unlink(request, pk, ci=None): @@ -322,8 +249,9 @@ def bestiary_link(request, pk): if request.method == 'POST': formset = QuantityForm(request.POST) bestiary = Bestiary.objects.filter(owner=request.user, id=pk).first() - mgmt = ['csrfmiddlewaretoken', 'form-TOTAL_FORMS', 'form-INITIAL_FORMS', 'form-MIN_NUM_FORMS', 'form-MAX_NUM_FORMS'] - for id,q in request.POST.items(): + mgmt = ['csrfmiddlewaretoken', 'form-TOTAL_FORMS', 'form-INITIAL_FORMS', 'form-MIN_NUM_FORMS', + 'form-MAX_NUM_FORMS'] + for id, q in request.POST.items(): if id in mgmt: continue try: @@ -335,33 +263,36 @@ def bestiary_link(request, pk): continue try: - creature = Creature.objects.filter(owner=request.user,id=id)[0] + creature = Creature.objects.filter(owner=request.user, id=id)[0] except: continue - quantity_obj = CreatureQuantity.objects.filter(creature=creature, bestiary=bestiary, owner=request.user).first() + quantity_obj = CreatureQuantity.objects.filter(creature=creature, bestiary=bestiary, + owner=request.user).first() # update or create - if quantity_obj: quantity_obj.quantity = q - else: quantity_obj = CreatureQuantity(creature=creature, bestiary=bestiary, owner=request.user, quantity=q) + if quantity_obj: + quantity_obj.quantity = q + else: + quantity_obj = CreatureQuantity(creature=creature, bestiary=bestiary, owner=request.user, quantity=q) # save quantity_obj.save() - logger.info("Expanded bestiary %s %s from user %s with new creatures!" % (bestiary.id, bestiary.name, bestiary.owner.id)) - return HttpResponseRedirect(reverse('bestiary-detail', kwargs={'pk':pk})) + logger.info("Expanded bestiary %s %s from user %s with new creatures!" % ( + bestiary.id, bestiary.name, bestiary.owner.id)) + return HttpResponseRedirect(reverse('bestiary-detail', kwargs={'pk': pk})) else: - context = {'qs':qs, 'formset': formset, 'form': formset} + context = {'qs': qs, 'formset': formset, 'form': formset} return render(request, 'bestiary_link.html', context=context) - class BestiaryListView(LoginRequiredMixin, generic.ListView): """Generic Bestiary list view.""" model = Bestiary def get_queryset(self): - #ok = Bestiary.objects.filter(owner=self.request.user).values('name', 'creaturequantity__quantity').aggregate(total_creatures=Sum('creaturequantity__quantity')) - return Bestiary.objects.filter(owner=self.request.user).values('name', 'id').annotate(total_creatures=Sum('creaturequantity__quantity')) - + # ok = Bestiary.objects.filter(owner=self.request.user).values('name', 'creaturequantity__quantity').aggregate(total_creatures=Sum('creaturequantity__quantity')) + return Bestiary.objects.filter(owner=self.request.user).values('name', 'id').annotate( + total_creatures=Sum('creaturequantity__quantity')) from .forms import BestiaryModifyForm, CreatureModifyForm @@ -371,13 +302,14 @@ def get_queryset(self): class CreatureCreate(LoginRequiredMixin, CreateView): """Generic create view.""" model = Creature - initial={'size':Creature.MEDIUM,'color': Creature.DARKGRAY,'position': Creature.WALKING,'show_name': True} + initial = {'size': Creature.MEDIUM, 'color': Creature.DARKGRAY, 'position': Creature.WALKING, 'show_name': True} form_class = CreatureModifyForm def get_form_kwargs(self): kwargs = super(CreatureCreate, self).get_form_kwargs() kwargs.update({'user': self.request.user}) return kwargs + def form_valid(self, form): user = self.request.user form.instance.owner = user @@ -386,14 +318,17 @@ def form_valid(self, form): logger.info("Creature %s created for user %s" % (form.instance.name, user.id)) return super(CreatureCreate, self).form_valid(form) + class CreatureUpdate(LoginRequiredMixin, UpdateView): """Generic update view.""" model = Creature form_class = CreatureModifyForm + def get_form_kwargs(self): kwargs = super(CreatureUpdate, self).get_form_kwargs() kwargs.update({'user': self.request.user}) return kwargs + def get_object(self, queryset=None): """ Hook to ensure object is owned by request.user. """ obj = super(CreatureUpdate, self).get_object() @@ -407,16 +342,18 @@ def form_valid(self, form): form_color = creature_form.color pk = creature_form.id db_color = Creature.objects.filter(owner=self.request.user, pk=pk).first().color - #print(form_color, db_color, self.request.user.groups.filter(name='Patrons').count()) + # print(form_color, db_color, self.request.user.groups.filter(name='Patrons').count()) # if db_color != form_color and self.request.user.groups.filter(name='Patrons').count() <= 0: # creature_form.color = db_color logger.info("Creature %s updated for user %s" % (form.instance.name, self.request.user.id)) return super(CreatureUpdate, self).form_valid(form) + class CreatureDelete(LoginRequiredMixin, DeleteView): """Generic creature delete view.""" model = Creature success_url = reverse_lazy('creatures') + def get_object(self, queryset=None): """ Hook to ensure object is owned by request.user. """ obj = super(CreatureDelete, self).get_object() @@ -424,26 +361,29 @@ def get_object(self, queryset=None): raise Http404 return obj + class CreatureAllDelete(LoginRequiredMixin, DeleteView): """Generic creature ALL delete view.""" model = Creature success_url = reverse_lazy('creatures') + def get_object(self, queryset=None): obj = Creature.objects.filter(owner=self.request.user) return obj - + + # Bestiary Forms class BestiaryCreate(LoginRequiredMixin, CreateView): """Generic bestiary create view.""" form_class = BestiaryModifyForm model = Bestiary + def form_valid(self, form): form.instance.owner = self.request.user logger.info("Bestiary %s created for user %s" % (form.instance.name, self.request.user.id)) return super(BestiaryCreate, self).form_valid(form) - @login_required() def create_ddb_enc_bestiary(request): """Dndbeyond Encounter bestiary create view.""" @@ -454,7 +394,7 @@ def create_ddb_enc_bestiary(request): # DDB API Endpoints DDB_ENCOUTNER_ENDPOINT = "https://encounter-service.dndbeyond.com/v1/encounters/" DDB_MONSTER_ENDPOINT = "https://monster-service.dndbeyond.com/v1/Monster?" - + # Try to get DDB Encounter data enc_url = form.cleaned_data.get('ddb_enc_url') enc_uuid = str(urlparse(enc_url).path).replace("/encounters/", "") @@ -482,7 +422,6 @@ def create_ddb_enc_bestiary(request): except requests.RequestException as exception: logger.error("Could not download DDB Monster Data, Error: \n %s" % exception) - # Create a bestiary bestiary = Bestiary() bestiary.name = str(enc_dict["data"]["name"]) @@ -501,7 +440,6 @@ def create_ddb_enc_bestiary(request): if i["isReleased"]: # This is a monster of the SRD or Publicly available if i["basicAvatarUrl"]: - # Seems DDB is publishing wrong URLs, fixing those here if urlparse(i["basicAvatarUrl"]).netloc == "www.dndbeyond.com.com": creature.img_url = "https://www.dndbeyond.com" + urlparse(i["basicAvatarUrl"]).path @@ -518,7 +456,7 @@ def create_ddb_enc_bestiary(request): else: creature.img_url = "https://media-waterdeep.cursecdn.com/avatars/4675/664/636747837303835953.jpeg" - # Determin correct size. Be aware this might change on ddb side + # Determine correct size. Be aware this might change on ddb side if i["sizeId"] == 2: creature.size = "T" if i["sizeId"] == 3: @@ -548,7 +486,7 @@ def create_ddb_enc_bestiary(request): bestiary_monsters.quantity = var["quantity"] bestiary_monsters.save() - + # If the create already exists, link it else: # Link monster @@ -558,19 +496,20 @@ def create_ddb_enc_bestiary(request): for var in enc_dict["data"]["monsters"]: if i["id"] == var["id"]: bestiary_monsters.quantity = var["quantity"] - bestiary_monsters.owner = request.user + bestiary_monsters.owner = request.user bestiary_monsters.save() return HttpResponseRedirect(reverse('bestiaries')) else: form = DDBEncounterBestiaryCreate() - return render(request, 'ddb_enc_bestiary.html', {'form': form}) + return render(request, 'ddb_enc_bestiary.html', {'form': form}) class BestiaryUpdate(LoginRequiredMixin, UpdateView): """Generic bestiary update view.""" model = Bestiary form_class = BestiaryModifyForm + def get_object(self, queryset=None): """ Hook to ensure object is owned by request.user. """ obj = super(BestiaryUpdate, self).get_object() @@ -578,10 +517,12 @@ def get_object(self, queryset=None): raise Http404 return obj + class BestiaryDelete(LoginRequiredMixin, DeleteView): """Generic bestiary delete view.""" model = Bestiary success_url = reverse_lazy('bestiaries') + def get_object(self, queryset=None): """ Hook to ensure object is owned by request.user. """ obj = super(BestiaryDelete, self).get_object() @@ -589,6 +530,7 @@ def get_object(self, queryset=None): raise Http404 return obj + # patreon def patreon(request): """Simple rendering of patreon info page.""" diff --git a/requirements.txt b/requirements.txt index f640edb..3035087 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,9 +1,10 @@ -Django==2.2.24 +Django==3.2.6 django-constrainedfilefield==4.0.0 django-widget-tweaks==1.4.8 -fake-useragent==0.1.11 +# fake-useragent==0.1.11 +https://github.com/schemen/fake-useragent/archive/refs/tags/0.1.12.zip # Use this fork until it's fixed opencv-python==4.5.3.56 -Pillow==8.3.1 +Pillow==8.3.2 sortedcontainers==2.4.0 psycopg2-binary==2.8.6 gunicorn==20.1.0 diff --git a/templates/index.html b/templates/index.html index eb4426a..52d56dd 100644 --- a/templates/index.html +++ b/templates/index.html @@ -1,7 +1,7 @@ {% extends "base_generic.html" %} {% block content %} - {% load staticfiles %} + {% load static %} diff --git a/templates/paperminis/json_form.html b/templates/paperminis/json_form.html index e34780e..7a9b9f6 100644 --- a/templates/paperminis/json_form.html +++ b/templates/paperminis/json_form.html @@ -22,7 +22,7 @@ Upload .json File - Choose a .json file with name, size and img_url attributes. The upload can take a while; be patient! + Choose a .json file. Check out this Example. You can create entirely RPG agnostic or DND-esque data. The upload can take a while; be patient! diff --git a/templates/patreon.html b/templates/patreon.html index 5ac0494..b520c92 100644 --- a/templates/patreon.html +++ b/templates/patreon.html @@ -2,7 +2,7 @@ {% block content %} - {% load staticfiles %} + {% load static %}
Choose a .json file with name, size and img_url attributes. The upload can take a while; be patient!
Choose a .json file. Check out this Example. You can create entirely RPG agnostic or DND-esque data. The upload can take a while; be patient!