├── .gitignore ├── App ├── PlayBooksApp │ ├── __init__.py │ ├── admin.py │ ├── apps.py │ ├── forms.py │ ├── markdown.py │ ├── migrations │ │ ├── 0001_initial.py │ │ ├── 0002_auto_20200328_1431.py │ │ ├── 0003_auto_20200328_1450.py │ │ ├── 0004_auto_20200329_1900.py │ │ ├── 0005_auto_20200401_1746.py │ │ ├── 0006_remove_playpage_offline_copy.py │ │ ├── 0007_auto_20200406_1814.py │ │ ├── 0008_remove_playpage_included_folder_path.py │ │ ├── 0009_auto_20200410_1315.py │ │ ├── 0010_auto_20200411_1101.py │ │ ├── 0011_playbook_creator.py │ │ ├── 0012_playbooksection_creator.py │ │ └── __init__.py │ ├── models.py │ ├── request.py │ ├── source_resolver.py │ ├── static │ │ ├── css │ │ │ ├── md.css │ │ │ └── style.css │ │ └── js │ │ │ └── navigation.js │ ├── templates │ │ ├── _edit_page.html │ │ ├── _page_content.html │ │ ├── _search_results.html │ │ ├── _select_server_file.html │ │ ├── base.html │ │ ├── index.html │ │ └── playbook.html │ ├── tests.py │ ├── urls.py │ └── views.py ├── PlayBooksWeb │ ├── __init__.py │ ├── apps.py │ ├── asgi.py │ ├── forms.py │ ├── migrations │ │ ├── 0001_initial.py │ │ ├── 0002_auto_20200404_1311.py │ │ └── __init__.py │ ├── models.py │ ├── settings.py │ ├── static │ │ ├── css │ │ │ └── login.css │ │ ├── external │ │ │ ├── css │ │ │ │ ├── bootstrap-select.css.map │ │ │ │ ├── bootstrap-select.min.css │ │ │ │ ├── bootstrap-toggle.min.css │ │ │ │ ├── bootstrap.min.css │ │ │ │ ├── bootstrap.min.css.map │ │ │ │ ├── fontawesome_all.min.css │ │ │ │ ├── jquery-confirm.min.css │ │ │ │ └── jquery-ui.min.css │ │ │ ├── js │ │ │ │ ├── bootstrap-select.js.map │ │ │ │ ├── bootstrap-select.min.js │ │ │ │ ├── bootstrap-select.min.js.map │ │ │ │ ├── bootstrap-toggle.min.js │ │ │ │ ├── bootstrap-toggle.min.js.map │ │ │ │ ├── bootstrap.min.js │ │ │ │ ├── bootstrap.min.js.map │ │ │ │ ├── jquery-confirm.min.js │ │ │ │ ├── jquery-ui.min.js │ │ │ │ ├── jquery.min.js │ │ │ │ └── popper.min.js │ │ │ ├── mdb │ │ │ │ ├── css │ │ │ │ │ ├── mdb.min.css │ │ │ │ │ └── mdb.min.css.map │ │ │ │ └── js │ │ │ │ │ ├── mdb.min.js │ │ │ │ │ └── mdb.min.js.map │ │ │ └── webfonts │ │ │ │ ├── fa-brands-400.eot │ │ │ │ ├── fa-brands-400.svg │ │ │ │ ├── fa-brands-400.ttf │ │ │ │ ├── fa-brands-400.woff │ │ │ │ ├── fa-brands-400.woff2 │ │ │ │ ├── fa-regular-400.eot │ │ │ │ ├── fa-regular-400.svg │ │ │ │ ├── fa-regular-400.ttf │ │ │ │ ├── fa-regular-400.woff │ │ │ │ ├── fa-regular-400.woff2 │ │ │ │ ├── fa-solid-900.eot │ │ │ │ ├── fa-solid-900.svg │ │ │ │ ├── fa-solid-900.ttf │ │ │ │ ├── fa-solid-900.woff │ │ │ │ └── fa-solid-900.woff2 │ │ ├── img │ │ │ ├── Favicon.xml │ │ │ ├── Logo.svg │ │ │ ├── Logo.xml │ │ │ └── favicon.ico │ │ └── js │ │ │ └── includeFolder.js │ ├── templates │ │ ├── _folder_index.html │ │ ├── admin │ │ │ ├── _base.html │ │ │ └── base_site.html │ │ ├── folder_change.html │ │ └── registration │ │ │ ├── logged_out.html │ │ │ ├── login.html │ │ │ └── registration_base.html │ ├── urls.py │ ├── validators.py │ ├── views.py │ └── wsgi.py ├── manage.py └── settings.json ├── Docker ├── Dockerfile └── README.md ├── README.md ├── SampleImages ├── Dashboard_Overview.png ├── Edit_Article_Example.png ├── HTTP_Upload.png ├── HTTP_Upload_Example.png ├── Server_Upload_Example.png ├── Text_Input_Example.png └── Update_Feature.png └── requirements.txt /.gitignore: -------------------------------------------------------------------------------- 1 | *.sqlite3 2 | *.pyc 3 | 4 | venv/* 5 | .vscode/* -------------------------------------------------------------------------------- /App/PlayBooksApp/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/csandker/Playbooks/1da352e6b584fe51fd3e758e5e2a404ebf299762/App/PlayBooksApp/__init__.py -------------------------------------------------------------------------------- /App/PlayBooksApp/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /App/PlayBooksApp/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class PlayBooksAppConfig(AppConfig): 5 | name = 'PlayBooksApp' 6 | -------------------------------------------------------------------------------- /App/PlayBooksApp/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | from django.forms import ModelForm, ChoiceField 3 | from django.core.validators import URLValidator 4 | from django.core.exceptions import ValidationError 5 | from django.utils.translation import gettext_lazy as _ 6 | from django.urls import reverse 7 | 8 | from .models import Playpage, PlaybookSection, Playbook 9 | from .request import URLRequester 10 | from .markdown import MarkdownParser 11 | from .source_resolver import SourceResolver 12 | 13 | from PlayBooksWeb.models import IncludedFolder 14 | 15 | 16 | class ChoiceFieldNoValidation(ChoiceField): 17 | DEFAULT_CHOICE = ( ('-', (('', '-- Choose Folder --'),)),) 18 | 19 | def validate(self, value): 20 | pass 21 | 22 | class PlaypageForm(ModelForm): 23 | INIT_SOURCE_TYPE = None 24 | 25 | class Meta: 26 | model = Playpage 27 | fields = ['title','source_type', 'check_updates'] 28 | help_texts = { 29 | 'title': _("Title of your Page. Keep this short but significant"), 30 | } 31 | widgets = { 32 | 'title': forms.TextInput(attrs={'required': '', 'class': 'form-control'}), 33 | 'check_updates': forms.CheckboxInput( attrs={'data-toggle': 'toggle', 'class': 'update-toggle'} ) 34 | } 35 | 36 | def __init__(self, user, *args, **kwargs): 37 | super(PlaypageForm, self).__init__(*args, **kwargs) 38 | if( not self.instance.pk ): 39 | self.instance.creator = user 40 | 41 | if( self.INIT_SOURCE_TYPE ): 42 | self.instance.source_type = self.INIT_SOURCE_TYPE 43 | self['source_type'].initial = self.INIT_SOURCE_TYPE 44 | 45 | ## HTTP UPLOAD FIELD 46 | http_initial = self.instance.source if ( self.instance and self.instance.source_type == Playpage.SOURCE_HTTP ) else '' 47 | http_source = forms.CharField(required=False, label="HTTP Resource", initial=http_initial ) 48 | http_source.help_text = _("Enter a HTTP URL to fetch Markdown Text from, e.g. Raw Github Pages.") 49 | http_source.widget = forms.TextInput(attrs={ 50 | "data-event": 'urlPrefetch', 51 | "data-url-prefetch": reverse('apiPrefetchSource'), 52 | 'class': 'full-width', 53 | "placeholder": "https://raw.githubusercontent.com/redcanaryco/atomic-red-team/master/atomics/T1170/T1170.md" 54 | }) 55 | self.fields['http_source'] = http_source 56 | ## FILE UPLOAD FIELD 57 | file_initial = self.instance.source if ( self.instance and self.instance.source_type == Playpage.SOURCE_UPLOAD ) else '' 58 | file_source = forms.FileField(required=False, label="File Upload", initial=file_initial) 59 | file_source.help_text = _("Upload a Markdown file") 60 | file_source.widget = forms.FileInput(attrs={ 61 | "data-event": 'urlPrefetch', 62 | "data-url-prefetch": reverse('apiPrefetchSource'), 63 | 'class': 'form-control' 64 | }) 65 | self.fields['file_source'] = file_source 66 | ## DISK UPLOAD FIELD 67 | ### DISK FILES 68 | if( 69 | self.instance.source_type == Playpage.SOURCE_DISK and 70 | self.instance and 71 | self.instance.included_folder and 72 | self.instance.included_folder.id and 73 | self.instance.source 74 | ): 75 | allowedExtensions = MarkdownParser.ALLOWED_FILE_EXTENSIONS 76 | included_folder_ID = self.instance.included_folder.id 77 | included_folder_path_choices = IncludedFolder.list_allowed_files(included_folder_ID, allowedExtensions) 78 | included_folder_path_initial = self.instance.source 79 | else: 80 | included_folder_path_choices = ChoiceFieldNoValidation.DEFAULT_CHOICE 81 | included_folder_path_initial = '' 82 | included_folder_path = ChoiceFieldNoValidation(required=False, label='File', initial=included_folder_path_initial) 83 | included_folder_path.help_text = _("Chose a File From Disk As Source") 84 | included_folder_path.widget = forms.Select(choices=included_folder_path_choices, attrs={ 85 | 'class': 'form-control', 86 | 'data-event': 'urlPrefetch', 87 | 'data-url-prefetch': reverse('apiPrefetchSource'), 88 | 'data-live-search': 'true' 89 | }) 90 | self.fields['included_folder_path'] = included_folder_path 91 | ### DISK Folders 92 | disk_folder_choices = [('', '-- Choose --')] + [ (folder.id, folder.name) for folder in IncludedFolder.objects.all() ] # 93 | included_folder_initial = self.instance.included_folder.id if ( self.instance.source_type == Playpage.SOURCE_DISK and self.instance and self.instance.included_folder ) else '' 94 | included_folder = forms.ChoiceField(required=False, label='Folder', initial=included_folder_initial) 95 | included_folder.choices = disk_folder_choices 96 | included_folder.help_text = _("Choose a Folder From Disk To Select a Markdown File") 97 | included_folder.widget = forms.Select(choices=disk_folder_choices, attrs={ 98 | 'data-event': 'diskFolder', 99 | 'data-update-target': self['included_folder_path'].auto_id, 100 | 'data-url-fileoptions': reverse('apiServerFileOptions'), 101 | 'class': 'form-control' 102 | }) 103 | self.fields['included_folder'] = included_folder 104 | ## PASTE UPLOAD 105 | text_source_initial = self.instance.offline_store if ( self.instance ) else '' 106 | text_source = forms.CharField(required=False, label=False, initial=text_source_initial ) 107 | text_source.help_text = _("Write Markdown Yourself") 108 | text_source.widget = forms.Textarea(attrs={ 109 | "data-event": 'urlPrefetch', 110 | "data-url-prefetch": reverse('apiPrefetchSource'), 111 | "placeholder": "Start Typing...", 112 | "class": "hidden" 113 | }) 114 | self.fields['text_source'] = text_source 115 | 116 | ## FIELD Order 117 | self.field_order = ['title', 'source_type', 'http_source', 'file_source', 'included_folder', 'included_folder_path', 'text_source', 'check_updates'] 118 | self.order_fields(self.field_order) 119 | 120 | self.source_options = [ 121 | {'name': 'HTTP', 'source_type': self.instance.SOURCE_HTTP, 'source_field': self['source_type'].auto_id, 122 | 'allowed_fields': [ 123 | self['title'].auto_id, self['check_updates'].auto_id, self['http_source'].auto_id 124 | ] 125 | }, 126 | {'name': 'FILE UPLOAD', 'source_type': self.instance.SOURCE_UPLOAD, 'source_field': self['source_type'].auto_id, 127 | 'allowed_fields': [ 128 | self['title'].auto_id, self['file_source'].auto_id 129 | ] 130 | } 131 | , 132 | {'name': 'FROM SERVER', 'source_type': self.instance.SOURCE_DISK , 'source_field': self['source_type'].auto_id, 133 | 'allowed_fields': [ 134 | self['title'].auto_id, self['check_updates'].auto_id, self['included_folder'].auto_id, self['included_folder_path'].auto_id 135 | ] 136 | }, 137 | {'name': 'TEXT INPUT', 'source_type': self.instance.SOURCE_TEXT , 'source_field': self['source_type'].auto_id, 138 | 'allowed_fields': [ 139 | self['title'].auto_id, self['text_source'].auto_id, "page_update_modal_editablediv" 140 | ] 141 | } 142 | 143 | ] 144 | 145 | 146 | 147 | def validate_file_source(self): 148 | file_source = self['file_source'].data 149 | ## check existing 150 | if( not file_source ): 151 | if( hasattr(self._errors, 'get') and not self._errors.get('file_source') ): 152 | emsg = "File Source is required" 153 | self.add_error('file_source', emsg) 154 | return False 155 | ## check file upload content type 156 | if( hasattr(file_source, 'content_type') and not (file_source.content_type in MarkdownParser.ALLOWED_MIME_TYPES) ): 157 | if( hasattr(self._errors, 'get') and not self._errors.get('file_source') ): 158 | emsg = "File Source MimeType is not allowed. Content Type is %s" %file_source.content_type 159 | self.add_error('file_source', emsg) 160 | return False 161 | ## check mime type based on content 162 | try: 163 | import magic 164 | import copy 165 | ## make a deep copy 166 | buffer_copy = copy.deepcopy(file_source) 167 | ## reset the read cursor 168 | buffer_copy.seek(0) 169 | prober = magic.Magic(mime=True) 170 | buffer = buffer_copy.read().decode('utf-8', errors='ignore') 171 | probed_content_type = prober.from_buffer(buffer) 172 | if( not (probed_content_type in MarkdownParser.ALLOWED_MIME_TYPES) ): 173 | if( hasattr(self._errors, 'get') and not self._errors.get('file_source') ): 174 | emsg = "File Source MimeType is not allowed. Mime Type is %s" %probed_content_type 175 | self.add_error('file_source', emsg) 176 | return False 177 | except: 178 | ## pass along 179 | pass 180 | 181 | return True 182 | 183 | def validate_http_source(self): 184 | url = self['http_source'].data 185 | ## Check if existing 186 | if( not url ): 187 | if( hasattr(self._errors, 'get') and not self._errors.get('http_source') ): 188 | emsg = "HTTP Resource is required" 189 | self.add_error('http_source', emsg) 190 | return False 191 | else: 192 | ## Check if valid URL 193 | urlvalidator = URLValidator(schemes=['http', 'https']) 194 | try: 195 | urlvalidator(url) 196 | except ValidationError as e: 197 | if( hasattr(self._errors, 'get') and not self._errors.get('http_source') ): 198 | emsg = "Invalid URL scheme. Only http:// https:// is allowed" 199 | self.add_error('http_source', emsg) 200 | return False 201 | else: 202 | requester = URLRequester(url) 203 | requester.request() 204 | valid_response = requester.is_valid_response() 205 | if( not valid_response ): 206 | ## Add error if not already added 207 | if( hasattr(self._errors, 'get') and not self._errors.get('http_source') ): 208 | status_code = requester.get_status_code() 209 | emsg = "Received Invalid Server Response. Status Code was: '%s'" %(status_code) 210 | self.add_error('http_source', emsg) 211 | return False 212 | return True 213 | 214 | def validate_included_folder(self): 215 | ## Validate folder 216 | include_fodlerID = self['included_folder'].data 217 | if( not include_fodlerID ): 218 | if( hasattr(self._errors, 'get') and not self._errors.get('included_folder') ): 219 | emsg = "Not a valid choice." 220 | self.add_error('included_folder', emsg) 221 | return False 222 | objs = IncludedFolder.objects.filter(pk=include_fodlerID) 223 | if objs.count() != 1: 224 | if( hasattr(self._errors, 'get') and not self._errors.get('included_folder') ): 225 | emsg = "Not a valid choice." 226 | self.add_error('included_folder', emsg) 227 | return False 228 | return True 229 | 230 | def validate_included_folder_path(self): 231 | ## Validate path 232 | include_folder_path = self['included_folder_path'].data 233 | if( not include_folder_path ): 234 | if( hasattr(self._errors, 'get') and not self._errors.get('included_folder_path') ): 235 | emsg = "Not a valid choice." 236 | self.add_error('included_folder_path', emsg) 237 | return False 238 | if( ("../" in include_folder_path) or ("..\\" in include_folder_path) ): 239 | if( hasattr(self._errors, 'get') and not self._errors.get('included_folder_path') ): 240 | emsg = "Illegal characters contained in choice." 241 | self.add_error('included_folder_path', emsg) 242 | return False 243 | 244 | ## Check included Folder existing 245 | included_folder = self['included_folder'].data 246 | if( not included_folder ): 247 | if( hasattr(self._errors, 'get') and not self._errors.get('included_folder_path') ): 248 | emsg = "No Folder has been selected" 249 | self.add_error('included_folder_path', emsg) 250 | return False 251 | 252 | ## Check file path 253 | included_folder_obj = IncludedFolder.objects.get( pk=included_folder ) 254 | folder_path = included_folder_obj.path 255 | folder_path += '' if folder_path.endswith('/') else '/' 256 | file_path = "%s%s" %(folder_path, include_folder_path) 257 | if( not IncludedFolder.valid_file_path(file_path) ): 258 | if( hasattr(self._errors, 'get') and not self._errors.get('included_folder_path') ): 259 | emsg = "File not found on Server" 260 | self.add_error('included_folder_path', emsg) 261 | return False 262 | return True 263 | 264 | def validate_text_source(self): 265 | text_source = self['text_source'].data 266 | if( not text_source ): 267 | if( hasattr(self._errors, 'get') and not self._errors.get('text_source') ): 268 | emsg = "Required Field" 269 | self.add_error('text_source', emsg) 270 | return False 271 | return True 272 | 273 | def is_valid(self): 274 | ### Custom Validations 275 | valid_super = super(ModelForm, self).is_valid() 276 | source_type = self['source_type'].data 277 | valid_source = self.validate_source_type(source_type) 278 | 279 | return (valid_super and valid_source) 280 | 281 | def validate_source_type(self, source_type): 282 | if( source_type == Playpage.SOURCE_UPLOAD): 283 | return self.validate_file_source() 284 | elif( source_type == Playpage.SOURCE_HTTP ): 285 | return self.validate_http_source() 286 | elif( source_type == Playpage.SOURCE_DISK ): 287 | return ( self.validate_included_folder() and self.validate_included_folder_path() ) 288 | elif( source_type == Playpage.SOURCE_TEXT ): 289 | return self.validate_text_source() 290 | 291 | def get_included_folder_path_options(self): 292 | return self.fields['included_folder_path'].widget.choices 293 | 294 | def clean(self): 295 | ## Parent Clean 296 | super().clean() 297 | ## Custom Cleaning 298 | source_type = self.cleaned_data['source_type'] 299 | ## Setting Data 300 | if( source_type == Playpage.SOURCE_UPLOAD): 301 | valid = self.validate_file_source() 302 | if( valid ): 303 | ## can't be updated 304 | self.instance.check_updates = False 305 | self.cleaned_data['check_updates'] = False 306 | ## can't have include folder 307 | self.instance.included_folder = None 308 | self.cleaned_data['included_folder'] = None 309 | ## content 310 | self.instance.source = self.cleaned_data['file_source'] 311 | self.instance.offline_store = self.cleaned_data['file_source'].read().decode('utf-8', errors='ignore') 312 | 313 | elif( source_type == Playpage.SOURCE_HTTP ): 314 | valid = self.validate_http_source() 315 | if( valid ): 316 | ## can't have include folder 317 | self.instance.included_folder = None 318 | self.cleaned_data['included_folder'] = None 319 | ## content 320 | url = self.cleaned_data.get('http_source', '') 321 | resolver = SourceResolver(self.instance) 322 | response = resolver.resolve_from_http(url) 323 | self.instance.source = self.cleaned_data['http_source'] 324 | if( response ): 325 | self.instance.offline_store = response 326 | 327 | elif( source_type == Playpage.SOURCE_DISK ): 328 | valid_included_folder = self.validate_included_folder() 329 | if( valid_included_folder ): 330 | selected_included_folder = self.cleaned_data['included_folder'] 331 | if( selected_included_folder ): 332 | allowedExtensions = MarkdownParser.ALLOWED_FILE_EXTENSIONS 333 | newChoices = IncludedFolder.list_allowed_files(selected_included_folder, allowedExtensions) 334 | self.fields['included_folder_path'].widget.choices = newChoices 335 | self.fields['included_folder_path'].choices = newChoices 336 | self.instance.included_folder = IncludedFolder.objects.get( pk=selected_included_folder ) 337 | else: 338 | ## Reset to default choice 339 | newChoices = ChoiceFieldNoValidation.DEFAULT_CHOICE 340 | self.fields['included_folder_path'].widget.choices = newChoices 341 | self.fields['included_folder_path'].choices = newChoices 342 | valid_included_folder_path = self.validate_included_folder_path() 343 | if( valid_included_folder and valid_included_folder_path ): 344 | selected_included_folder = self.cleaned_data['included_folder'] 345 | selected_included_folder_path = self.cleaned_data['included_folder_path'] 346 | if( selected_included_folder_path ): 347 | self.instance.source = selected_included_folder_path 348 | resolver = SourceResolver(self.instance) 349 | content = resolver.resolve_from_disk(selected_included_folder, selected_included_folder_path) 350 | if( content ): 351 | self.instance.offline_store = content 352 | 353 | elif( source_type == Playpage.SOURCE_TEXT ): 354 | valid_text_source = self.validate_text_source() 355 | if( valid_text_source ): 356 | ## Keep original source type if exiting 357 | if( self.instance.pk ): 358 | self.cleaned_data['source_type'] = self.instance.source_type 359 | ## offline store 360 | self.instance.offline_store = self.cleaned_data['text_source'] 361 | 362 | return self.cleaned_data 363 | 364 | 365 | class TEXTPLaypageForm(PlaypageForm): 366 | INIT_SOURCE_TYPE = Playpage.SOURCE_TEXT 367 | 368 | 369 | class PlaybookSectionForm(ModelForm): 370 | 371 | class Meta: 372 | model = PlaybookSection 373 | fields = ['name'] 374 | help_texts = { 375 | 'name': _("Enter a name for a new Section"), 376 | } 377 | widgets = {'name': forms.TextInput( 378 | attrs = { 379 | 'autocomplete': 'off', 380 | 'placeholder': 'New Section Name', 381 | 'class': 'form-control' 382 | } 383 | )} 384 | 385 | def __init__(self, user, *args, **kwargs): 386 | super(PlaybookSectionForm, self).__init__(*args, **kwargs) 387 | if( not self.instance.pk ): 388 | self.instance.creator = user 389 | 390 | class PlaybookAddPageForm(forms.Form): 391 | page = forms.ChoiceField( 392 | label = "Add Existing Page", 393 | help_text = "Add existing Page", 394 | widget = forms.Select( 395 | attrs = { 396 | 'class': 'form-control selectpicker', 397 | 'data-live-search': 'true' 398 | } 399 | ) 400 | ) 401 | 402 | def __init__(self, user, *args, **kwargs): 403 | super(PlaybookAddPageForm, self).__init__(*args, **kwargs) 404 | accessible_pages = Playpage.accessible_pages(user) 405 | choices = [ (page.id, page.title) for page in accessible_pages ] 406 | self.fields['page'].choices = choices 407 | self.fields['page'].widget.choices = choices 408 | 409 | class PlaybookForm(ModelForm): 410 | class Meta: 411 | model = Playbook 412 | fields = ['name'] 413 | help_texts = { 414 | 'name': _("Enter a name for a new PlayBook"), 415 | } 416 | widgets = {'name': forms.TextInput( 417 | attrs = { 418 | 'autocomplete': 'off', 419 | 'placeholder': 'New Playbook Name', 420 | 'class': 'form-control' 421 | } 422 | )} 423 | 424 | 425 | def __init__(self, user, *args, **kwargs): 426 | super(PlaybookForm, self).__init__(*args, **kwargs) 427 | if( not self.instance.pk ): 428 | self.instance.creator = user -------------------------------------------------------------------------------- /App/PlayBooksApp/markdown.py: -------------------------------------------------------------------------------- 1 | import markdown2 2 | import codecs 3 | import re 4 | 5 | class MarkdownParser(): 6 | EXTRAS = [ 7 | "fenced-code-blocks", 8 | "cuddled-lists", 9 | "code-friendly", 10 | "numbering", 11 | "smarty-pants", 12 | "tables", 13 | "task_list" 14 | ] 15 | ALLOWED_MIME_TYPES = [ 16 | 'text/plain', 17 | 'text/markdown' 18 | ] 19 | ALLOWED_FILE_EXTENSIONS = [ 20 | 'md', 21 | 'txt' 22 | ] 23 | IMG_REPLACE_PATCH = 'data-action="replace-image" ' 24 | IMG_REPLACE_CLASS = "action image-replace " 25 | 26 | def __init__(self, extras=None): 27 | self.extras = extras or self.EXTRAS 28 | 29 | def parseMD(self, mdText): 30 | markdowner = markdown2.Markdown(self.extras) 31 | html = markdowner.convert(mdText) 32 | 33 | return html 34 | 35 | def replaceImages(self, content, path_validation_callback, path_processing_callback, **kwargs): 36 | ## try to contained images 37 | pImgTag = re.compile(r"\!\[.+\]\(.+\)") 38 | pImgLoc = re.compile(r"\(.+\)$") 39 | for match in pImgTag.finditer(content): 40 | ## get image tag ![..](.../...) 41 | mdImageTag = match.group() 42 | ## grab out image location (.../...) 43 | mdImgLocTag = pImgLoc.search(mdImageTag).group() 44 | ## strip out '(' and ')' 45 | imgSubPath = mdImgLocTag.lstrip('(').rstrip(')') 46 | ## path validation 47 | try: 48 | path_valid = path_validation_callback( imgSubPath, **kwargs) 49 | if( path_valid ): 50 | base64Img = path_processing_callback( imgSubPath, **kwargs ) 51 | if( base64Img ): 52 | imgTag = "%s" %(base64Img, imgSubPath) 53 | content = content.replace(mdImageTag, imgTag) 54 | except Exception as e: 55 | pass 56 | 57 | return content 58 | -------------------------------------------------------------------------------- /App/PlayBooksApp/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.0.4 on 2020-03-27 15:30 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | initial = True 9 | 10 | dependencies = [ 11 | ] 12 | 13 | operations = [ 14 | migrations.CreateModel( 15 | name='Playbook', 16 | fields=[ 17 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 18 | ('name', models.CharField(max_length=500)), 19 | ], 20 | ), 21 | ] 22 | -------------------------------------------------------------------------------- /App/PlayBooksApp/migrations/0002_auto_20200328_1431.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.0.4 on 2020-03-28 13:31 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('PlayBooksApp', '0001_initial'), 11 | ] 12 | 13 | operations = [ 14 | migrations.CreateModel( 15 | name='PlaybookSection', 16 | fields=[ 17 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 18 | ('name', models.CharField(max_length=100)), 19 | ], 20 | ), 21 | migrations.CreateModel( 22 | name='Playpage', 23 | fields=[ 24 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 25 | ('title', models.CharField(max_length=500)), 26 | ('created_at', models.DateTimeField(auto_now_add=True, null=True)), 27 | ('last_modified', models.DateTimeField(auto_now=True, null=True)), 28 | ], 29 | ), 30 | migrations.AddField( 31 | model_name='playbook', 32 | name='cover_img', 33 | field=models.TextField(default=''), 34 | ), 35 | migrations.AddField( 36 | model_name='playbook', 37 | name='created_at', 38 | field=models.DateTimeField(auto_now_add=True, null=True), 39 | ), 40 | migrations.AddField( 41 | model_name='playbook', 42 | name='last_modified', 43 | field=models.DateTimeField(auto_now=True, null=True), 44 | ), 45 | migrations.CreateModel( 46 | name='SectionContent', 47 | fields=[ 48 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 49 | ('position', models.PositiveIntegerField()), 50 | ('playpage', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='PlayBooksApp.Playpage')), 51 | ('section', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='PlayBooksApp.PlaybookSection')), 52 | ], 53 | ), 54 | migrations.AddField( 55 | model_name='playbooksection', 56 | name='pages', 57 | field=models.ManyToManyField(related_name='sections', related_query_name='section', through='PlayBooksApp.SectionContent', to='PlayBooksApp.Playpage'), 58 | ), 59 | migrations.AddField( 60 | model_name='playbooksection', 61 | name='playbook', 62 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='sections', related_query_name='section', to='PlayBooksApp.Playbook'), 63 | ), 64 | ] 65 | -------------------------------------------------------------------------------- /App/PlayBooksApp/migrations/0003_auto_20200328_1450.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.0.4 on 2020-03-28 13:50 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('PlayBooksApp', '0002_auto_20200328_1431'), 10 | ] 11 | 12 | operations = [ 13 | migrations.RemoveField( 14 | model_name='sectioncontent', 15 | name='position', 16 | ), 17 | migrations.AddField( 18 | model_name='playbooksection', 19 | name='section_position', 20 | field=models.PositiveIntegerField(blank=True, null=True), 21 | ), 22 | migrations.AddField( 23 | model_name='sectioncontent', 24 | name='page_position', 25 | field=models.PositiveIntegerField(blank=True, null=True), 26 | ), 27 | ] 28 | -------------------------------------------------------------------------------- /App/PlayBooksApp/migrations/0004_auto_20200329_1900.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.0.4 on 2020-03-29 17:00 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('PlayBooksApp', '0003_auto_20200328_1450'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='playpage', 15 | name='check_updates', 16 | field=models.BooleanField(default=True), 17 | ), 18 | migrations.AddField( 19 | model_name='playpage', 20 | name='http_source', 21 | field=models.TextField(blank=True, null=True), 22 | ), 23 | migrations.AddField( 24 | model_name='playpage', 25 | name='offline_copy', 26 | field=models.BooleanField(default=False), 27 | ), 28 | migrations.AlterField( 29 | model_name='playbook', 30 | name='cover_img', 31 | field=models.TextField(blank=True, null=True), 32 | ), 33 | ] 34 | -------------------------------------------------------------------------------- /App/PlayBooksApp/migrations/0005_auto_20200401_1746.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.0.4 on 2020-04-01 15:46 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('PlayBooksApp', '0004_auto_20200329_1900'), 10 | ] 11 | 12 | operations = [ 13 | migrations.RenameField( 14 | model_name='playpage', 15 | old_name='http_source', 16 | new_name='source', 17 | ), 18 | migrations.AddField( 19 | model_name='playpage', 20 | name='offline_store', 21 | field=models.TextField(blank=True, null=True), 22 | ), 23 | migrations.AddField( 24 | model_name='playpage', 25 | name='source_type', 26 | field=models.CharField(default='HTTP', max_length=500), 27 | ), 28 | ] 29 | -------------------------------------------------------------------------------- /App/PlayBooksApp/migrations/0006_remove_playpage_offline_copy.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.0.4 on 2020-04-04 11:12 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('PlayBooksApp', '0005_auto_20200401_1746'), 10 | ] 11 | 12 | operations = [ 13 | migrations.RemoveField( 14 | model_name='playpage', 15 | name='offline_copy', 16 | ), 17 | ] 18 | -------------------------------------------------------------------------------- /App/PlayBooksApp/migrations/0007_auto_20200406_1814.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.0.4 on 2020-04-06 16:14 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('PlayBooksWeb', '0002_auto_20200404_1311'), 11 | ('PlayBooksApp', '0006_remove_playpage_offline_copy'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AddField( 16 | model_name='playpage', 17 | name='included_folder', 18 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='PlayBooksWeb.IncludedFolder'), 19 | ), 20 | migrations.AddField( 21 | model_name='playpage', 22 | name='included_folder_path', 23 | field=models.TextField(blank=True, null=True), 24 | ), 25 | ] 26 | -------------------------------------------------------------------------------- /App/PlayBooksApp/migrations/0008_remove_playpage_included_folder_path.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.0.4 on 2020-04-06 18:17 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('PlayBooksApp', '0007_auto_20200406_1814'), 10 | ] 11 | 12 | operations = [ 13 | migrations.RemoveField( 14 | model_name='playpage', 15 | name='included_folder_path', 16 | ), 17 | ] 18 | -------------------------------------------------------------------------------- /App/PlayBooksApp/migrations/0009_auto_20200410_1315.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.0.4 on 2020-04-10 11:15 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('PlayBooksApp', '0008_remove_playpage_included_folder_path'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='playpage', 15 | name='check_updates', 16 | field=models.BooleanField(default=False), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /App/PlayBooksApp/migrations/0010_auto_20200411_1101.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.0.4 on 2020-04-11 09:01 2 | 3 | from django.conf import settings 4 | from django.db import migrations, models 5 | import django.db.models.deletion 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 12 | ('PlayBooksApp', '0009_auto_20200410_1315'), 13 | ] 14 | 15 | operations = [ 16 | migrations.AlterModelOptions( 17 | name='playbooksection', 18 | options={'ordering': ['section_position']}, 19 | ), 20 | migrations.AlterModelOptions( 21 | name='sectioncontent', 22 | options={'ordering': ['page_position']}, 23 | ), 24 | migrations.AddField( 25 | model_name='playpage', 26 | name='creator', 27 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL), 28 | ), 29 | migrations.AlterField( 30 | model_name='sectioncontent', 31 | name='page_position', 32 | field=models.PositiveIntegerField(default=0), 33 | ), 34 | migrations.AlterField( 35 | model_name='sectioncontent', 36 | name='playpage', 37 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='section_contents', related_query_name='section_contents', to='PlayBooksApp.Playpage'), 38 | ), 39 | migrations.AlterField( 40 | model_name='sectioncontent', 41 | name='section', 42 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='section_contents', related_query_name='section_contents', to='PlayBooksApp.PlaybookSection'), 43 | ), 44 | ] 45 | -------------------------------------------------------------------------------- /App/PlayBooksApp/migrations/0011_playbook_creator.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.0.4 on 2020-04-11 09:29 2 | 3 | from django.conf import settings 4 | from django.db import migrations, models 5 | import django.db.models.deletion 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 12 | ('PlayBooksApp', '0010_auto_20200411_1101'), 13 | ] 14 | 15 | operations = [ 16 | migrations.AddField( 17 | model_name='playbook', 18 | name='creator', 19 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL), 20 | ), 21 | ] 22 | -------------------------------------------------------------------------------- /App/PlayBooksApp/migrations/0012_playbooksection_creator.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.0.4 on 2020-04-11 12:18 2 | 3 | from django.conf import settings 4 | from django.db import migrations, models 5 | import django.db.models.deletion 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 12 | ('PlayBooksApp', '0011_playbook_creator'), 13 | ] 14 | 15 | operations = [ 16 | migrations.AddField( 17 | model_name='playbooksection', 18 | name='creator', 19 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL), 20 | ), 21 | ] 22 | -------------------------------------------------------------------------------- /App/PlayBooksApp/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/csandker/Playbooks/1da352e6b584fe51fd3e758e5e2a404ebf299762/App/PlayBooksApp/migrations/__init__.py -------------------------------------------------------------------------------- /App/PlayBooksApp/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.contrib.auth.models import User 3 | 4 | 5 | from PlayBooksWeb.models import IncludedFolder 6 | 7 | class Playbook(models.Model): 8 | name = models.CharField(max_length=500) 9 | cover_img = models.TextField(null=True, blank=True) 10 | creator = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True) 11 | created_at = models.DateTimeField(auto_now_add=True, null=True, blank=True) 12 | last_modified = models.DateTimeField(auto_now=True, null=True, blank=True) 13 | 14 | @classmethod 15 | def user_all(self, user): 16 | return self.objects.filter(creator=user) 17 | 18 | def user_sections(self, user): 19 | return self.sections.filter(creator=user) 20 | 21 | ## Only creator can delete Playbook 22 | def can_delete(self, user): 23 | return (user == self.creator) 24 | 25 | def __str__(self): 26 | return "name='%s', created='%s', last_modified='%s'" %(self.name, self.created_at, self.last_modified) 27 | 28 | 29 | class Playpage(models.Model): 30 | SOURCE_HTTP = "HTTP" 31 | SOURCE_UPLOAD = "UPLOAD" 32 | SOURCE_DISK = "DISK" 33 | SOURCE_TEXT = "TEXT" 34 | 35 | title = models.CharField(max_length=500) 36 | source = models.TextField(null=True, blank=True) 37 | source_type = models.CharField(max_length=500, default=SOURCE_HTTP) 38 | offline_store = models.TextField(null=True, blank=True) 39 | included_folder = models.ForeignKey(IncludedFolder, on_delete=models.SET_NULL, null=True, blank=True) 40 | check_updates = models.BooleanField(default=False) 41 | creator = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True) 42 | created_at = models.DateTimeField(auto_now_add=True, null=True, blank=True) 43 | last_modified = models.DateTimeField(auto_now=True, null=True, blank=True) 44 | 45 | ## Access = Read + Write 46 | def can_access(self, user): 47 | return ( user == self.creator ) 48 | 49 | ## Change Position 50 | def can_change_position(self, user): 51 | return self.can_access(user) 52 | 53 | ## Only creator can delete Pages 54 | def can_delete(self, user): 55 | return (user == self.creator) 56 | 57 | def can_be_updated(self): 58 | return self.source_type in [self.SOURCE_DISK, self.SOURCE_HTTP] 59 | 60 | def playbooks(self): 61 | return Playbook.objects.filter(section__pages__id=self.id) 62 | 63 | @classmethod 64 | def accessible_pages(self, user): 65 | return [ page for page in self.objects.all() if page.can_access(user) ] 66 | 67 | def save_model(self, request, obj, form, change): 68 | if( request.user and not obj.pk ): 69 | # Only set added_by during the first save. 70 | obj.creator = request.user 71 | super().save_model(request, obj, form, change) 72 | 73 | 74 | def set_position(self, position, sectionID): 75 | try: 76 | section_content = self.section_contents.get(section=sectionID) 77 | section_content.page_position = position 78 | section_content.save() 79 | except Exception as e: 80 | return e 81 | else: 82 | return True 83 | 84 | def __str__(self): 85 | return "title='%s', created='%s', last_modified='%s'" %(self.title, self.created_at, self.last_modified) 86 | 87 | 88 | class PlaybookSection(models.Model): 89 | class Meta: 90 | ordering = ['section_position'] 91 | 92 | name = models.CharField(max_length=100) 93 | section_position = models.PositiveIntegerField(null=True, blank=True) 94 | creator = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True) 95 | playbook = models.ForeignKey( 96 | Playbook, 97 | on_delete=models.CASCADE, 98 | related_name='sections', 99 | related_query_name="section",) 100 | pages = models.ManyToManyField( 101 | Playpage, 102 | through='SectionContent', through_fields=('section', 'playpage'), 103 | related_name='sections', 104 | related_query_name="section") 105 | 106 | ## Only creator can delete Section 107 | def can_delete(self, user): 108 | return (user == self.creator) 109 | 110 | def pages_sorted(self): 111 | return self.pages.all().order_by('section_contents__page_position') 112 | 113 | def last_page_position(self): 114 | position = 0 115 | try: 116 | position = self.section_contents.order_by('page_position').last().page_position 117 | except: 118 | ## if error, pass and go with 0 119 | pass 120 | return position 121 | 122 | def append_page(self, page): 123 | position = self.last_page_position() or 0 124 | return self.pages.add( page, through_defaults={ 'page_position': position }) 125 | 126 | def __str__(self): 127 | return "name='%s' position='%s'" %(self.name, self.section_position) 128 | 129 | 130 | class SectionContent(models.Model): 131 | class Meta: 132 | ordering = ['page_position'] 133 | 134 | section = models.ForeignKey( 135 | PlaybookSection, 136 | on_delete=models.CASCADE, 137 | related_name='section_contents', 138 | related_query_name="section_contents" 139 | ) 140 | playpage = models.ForeignKey( 141 | Playpage, 142 | on_delete=models.CASCADE, 143 | related_name='section_contents', 144 | related_query_name="section_contents" 145 | ) 146 | page_position = models.PositiveIntegerField(default=0) 147 | 148 | -------------------------------------------------------------------------------- /App/PlayBooksApp/request.py: -------------------------------------------------------------------------------- 1 | import requests 2 | from urllib.parse import urlparse 3 | import base64 4 | 5 | from .markdown import MarkdownParser 6 | 7 | class URLRequester(): 8 | HEADERS = { 9 | 'User-Agent': 'Mozilla/5.0 (X11; Linux; rv:31.0) Gecko/20100101 Firefox' 10 | } 11 | 12 | ALLOWED_IMAGE_CONTENT_TYPES = [ 13 | 'image/jpeg', 14 | 'image/png' 15 | ] 16 | 17 | def __init__(self, url): 18 | self.url = url 19 | self.verify = False ## Do not verify per default 20 | self.response = None 21 | self.status_code = None 22 | 23 | def request(self, url=None): 24 | response = self.request_url(url=url) 25 | self.status_code = response.status_code 26 | self.response = response.text 27 | return response 28 | 29 | def is_valid_response(self): 30 | if( self.get_status_code() == 200 ): 31 | return True 32 | else: 33 | return False 34 | 35 | def request_url(self, url=None): 36 | request_url = url or self.url 37 | response = requests.get(url=request_url, headers=self.HEADERS, verify=self.verify) 38 | return response 39 | 40 | def resolve_images(self): 41 | md_parser = MarkdownParser() 42 | content = md_parser.replaceImages(self.response, self.path_validation, self.path_processing) 43 | self.response = content 44 | 45 | def request_subpath(self, path): 46 | parsedUrl = urlparse(self.url) 47 | subPaths = [ subPath for subPath in urlparse(self.url).path.split('/') if subPath ] 48 | for subPath in subPaths: 49 | subPath += '/' if subPath.endswith('') else '' 50 | subPath += path 51 | rootPath = parsedUrl.netloc 52 | rootPath += '/' if rootPath.endswith('') else '' 53 | full_url = "%s://%s%s" %(parsedUrl.scheme, rootPath, subPath) 54 | response = self.request_url(url=full_url) 55 | if( response.status_code == 200 ): 56 | return response 57 | 58 | def path_validation(self, path): 59 | response = self.request_subpath(path) 60 | if( response.headers['Content-Type'] in self.ALLOWED_IMAGE_CONTENT_TYPES ): 61 | return True 62 | else: 63 | return False 64 | 65 | 66 | def path_processing(self, path): 67 | response = self.request_subpath(path) 68 | if( response.content ): 69 | return base64.b64encode(response.content).decode('ascii') 70 | else: 71 | return False 72 | 73 | 74 | def get_response(self): 75 | return self.response 76 | 77 | def get_status_code(self): 78 | return self.status_code -------------------------------------------------------------------------------- /App/PlayBooksApp/source_resolver.py: -------------------------------------------------------------------------------- 1 | 2 | from .models import Playpage 3 | from PlayBooksWeb.models import IncludedFolder 4 | 5 | from .request import URLRequester 6 | from .markdown import MarkdownParser 7 | 8 | class SourceResolver(): 9 | 10 | def __init__(self, page): 11 | if( page and isinstance(page, Playpage) ): 12 | self.playpage = page 13 | else: 14 | raise Exception("Invalid Page Given To Source Resolver") 15 | 16 | def resolve_type(self): 17 | source_type = self.playpage.source_type 18 | if( source_type == Playpage.SOURCE_HTTP ): 19 | return self.resolve_from_http(url=self.playpage.source) 20 | elif( source_type == Playpage.SOURCE_DISK ): 21 | return self.resolve_from_disk(self.playpage.included_folder.id, self.playpage.source) 22 | 23 | def resolve_from_http(self, url=None): 24 | if( url ): 25 | requester = URLRequester(url) 26 | requester.request() 27 | if( requester.is_valid_response() ): 28 | requester.resolve_images() 29 | response = requester.get_response() 30 | return response 31 | else: 32 | return False 33 | else: 34 | return False 35 | 36 | def resolve_from_disk(self, selected_included_folder, selected_included_folder_path): 37 | md_parser = MarkdownParser() 38 | content = IncludedFolder.get_file_content(selected_included_folder, selected_included_folder_path) 39 | content = md_parser.replaceImages(content, IncludedFolder.path_validation, IncludedFolder.path_processing, folderID=selected_included_folder, subpath=selected_included_folder_path) 40 | if( content ): 41 | return content 42 | else: 43 | return False 44 | -------------------------------------------------------------------------------- /App/PlayBooksApp/static/css/md.css: -------------------------------------------------------------------------------- 1 | .codehilite .hll { background-color: #ffffcc } 2 | .codehilite { background: #f8f8f8; } 3 | .codehilite .c { color: #408080; font-style: italic } /* Comment */ 4 | .codehilite .err { border: 1px solid #FF0000 } /* Error */ 5 | .codehilite .k { color: #008000; font-weight: bold } /* Keyword */ 6 | .codehilite .o { color: #666666 } /* Operator */ 7 | .codehilite .ch { color: #408080; font-style: italic } /* Comment.Hashbang */ 8 | .codehilite .cm { color: #408080; font-style: italic } /* Comment.Multiline */ 9 | .codehilite .cp { color: #BC7A00 } /* Comment.Preproc */ 10 | .codehilite .cpf { color: #408080; font-style: italic } /* Comment.PreprocFile */ 11 | .codehilite .c1 { color: #408080; font-style: italic } /* Comment.Single */ 12 | .codehilite .cs { color: #408080; font-style: italic } /* Comment.Special */ 13 | .codehilite .gd { color: #A00000 } /* Generic.Deleted */ 14 | .codehilite .ge { font-style: italic } /* Generic.Emph */ 15 | .codehilite .gr { color: #FF0000 } /* Generic.Error */ 16 | .codehilite .gh { color: #000080; font-weight: bold } /* Generic.Heading */ 17 | .codehilite .gi { color: #00A000 } /* Generic.Inserted */ 18 | .codehilite .go { color: #888888 } /* Generic.Output */ 19 | .codehilite .gp { color: #000080; font-weight: bold } /* Generic.Prompt */ 20 | .codehilite .gs { font-weight: bold } /* Generic.Strong */ 21 | .codehilite .gu { color: #800080; font-weight: bold } /* Generic.Subheading */ 22 | .codehilite .gt { color: #0044DD } /* Generic.Traceback */ 23 | .codehilite .kc { color: #008000; font-weight: bold } /* Keyword.Constant */ 24 | .codehilite .kd { color: #008000; font-weight: bold } /* Keyword.Declaration */ 25 | .codehilite .kn { color: #008000; font-weight: bold } /* Keyword.Namespace */ 26 | .codehilite .kp { color: #008000 } /* Keyword.Pseudo */ 27 | .codehilite .kr { color: #008000; font-weight: bold } /* Keyword.Reserved */ 28 | .codehilite .kt { color: #B00040 } /* Keyword.Type */ 29 | .codehilite .m { color: #666666 } /* Literal.Number */ 30 | .codehilite .s { color: #BA2121 } /* Literal.String */ 31 | .codehilite .na { color: #7D9029 } /* Name.Attribute */ 32 | .codehilite .nb { color: #008000 } /* Name.Builtin */ 33 | .codehilite .nc { color: #0000FF; font-weight: bold } /* Name.Class */ 34 | .codehilite .no { color: #880000 } /* Name.Constant */ 35 | .codehilite .nd { color: #AA22FF } /* Name.Decorator */ 36 | .codehilite .ni { color: #999999; font-weight: bold } /* Name.Entity */ 37 | .codehilite .ne { color: #D2413A; font-weight: bold } /* Name.Exception */ 38 | .codehilite .nf { color: #0000FF } /* Name.Function */ 39 | .codehilite .nl { color: #A0A000 } /* Name.Label */ 40 | .codehilite .nn { color: #0000FF; font-weight: bold } /* Name.Namespace */ 41 | .codehilite .nt { color: #008000; font-weight: bold } /* Name.Tag */ 42 | .codehilite .nv { color: #19177C } /* Name.Variable */ 43 | .codehilite .ow { color: #AA22FF; font-weight: bold } /* Operator.Word */ 44 | .codehilite .w { color: #bbbbbb } /* Text.Whitespace */ 45 | .codehilite .mb { color: #666666 } /* Literal.Number.Bin */ 46 | .codehilite .mf { color: #666666 } /* Literal.Number.Float */ 47 | .codehilite .mh { color: #666666 } /* Literal.Number.Hex */ 48 | .codehilite .mi { color: #666666 } /* Literal.Number.Integer */ 49 | .codehilite .mo { color: #666666 } /* Literal.Number.Oct */ 50 | .codehilite .sa { color: #BA2121 } /* Literal.String.Affix */ 51 | .codehilite .sb { color: #BA2121 } /* Literal.String.Backtick */ 52 | .codehilite .sc { color: #BA2121 } /* Literal.String.Char */ 53 | .codehilite .dl { color: #BA2121 } /* Literal.String.Delimiter */ 54 | .codehilite .sd { color: #BA2121; font-style: italic } /* Literal.String.Doc */ 55 | .codehilite .s2 { color: #BA2121 } /* Literal.String.Double */ 56 | .codehilite .se { color: #BB6622; font-weight: bold } /* Literal.String.Escape */ 57 | .codehilite .sh { color: #BA2121 } /* Literal.String.Heredoc */ 58 | .codehilite .si { color: #BB6688; font-weight: bold } /* Literal.String.Interpol */ 59 | .codehilite .sx { color: #008000 } /* Literal.String.Other */ 60 | .codehilite .sr { color: #BB6688 } /* Literal.String.Regex */ 61 | .codehilite .s1 { color: #BA2121 } /* Literal.String.Single */ 62 | .codehilite .ss { color: #19177C } /* Literal.String.Symbol */ 63 | .codehilite .bp { color: #008000 } /* Name.Builtin.Pseudo */ 64 | .codehilite .fm { color: #0000FF } /* Name.Function.Magic */ 65 | .codehilite .vc { color: #19177C } /* Name.Variable.Class */ 66 | .codehilite .vg { color: #19177C } /* Name.Variable.Global */ 67 | .codehilite .vi { color: #19177C } /* Name.Variable.Instance */ 68 | .codehilite .vm { color: #19177C } /* Name.Variable.Magic */ 69 | .codehilite .il { color: #666666 } /* Literal.Number.Integer.Long */ 70 | -------------------------------------------------------------------------------- /App/PlayBooksApp/static/css/style.css: -------------------------------------------------------------------------------- 1 | html, body, .container-fluid { 2 | height: 100%; 3 | } 4 | 5 | body .overflow { 6 | overflow: auto; 7 | height: 100%; 8 | } 9 | 10 | body .overflow-overlay { 11 | overflow: overlay; 12 | } 13 | 14 | body .hidden { 15 | display: none; 16 | } 17 | 18 | body .full-width { 19 | width: 100%; 20 | } 21 | 22 | body .warning { 23 | color: #f50404; 24 | font-weight: bold; 25 | } 26 | 27 | body .contenteditable { 28 | width: 100%; 29 | background: #eae9e9; 30 | min-height: 80px; 31 | box-shadow: inset 0 5px 5px rgba(0,0,0,.125); 32 | padding: 10px; 33 | } 34 | 35 | .btn.btn-add { 36 | color: #fff; 37 | background-color: #18692a; 38 | border-color: #041d04; 39 | } 40 | .btn.btn-add:hover { 41 | color: #fff; 42 | background-color: #114a1d; 43 | } 44 | 45 | .btn.btn-delete { 46 | color: #fff; 47 | background-color: #5f1010; 48 | border-color: #250609; 49 | } 50 | .btn.btn-delete:hover { 51 | color: #fff; 52 | background-color: #520c13; 53 | } 54 | 55 | body img { 56 | max-width: 100%; 57 | display: block; 58 | } 59 | 60 | /* width */ 61 | ::-webkit-scrollbar { 62 | width: 0.5em; 63 | height: 0.8em; 64 | } 65 | 66 | /* Track */ 67 | ::-webkit-scrollbar-track { 68 | /*box-shadow: inset 0 0 5px grey;*/ 69 | border-radius: 10px; 70 | box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3); 71 | } 72 | 73 | /* Handle */ 74 | ::-webkit-scrollbar-thumb { 75 | /*background: red; */ 76 | border-radius: 10px; 77 | background-color: darkgrey; 78 | outline: 1px solid slategrey; 79 | } 80 | 81 | /* Handle on hover */ 82 | ::-webkit-scrollbar-thumb:hover { 83 | background: #1c1d22; 84 | } 85 | 86 | body #PlayBooks { 87 | height: 100%; 88 | font-family: 'Avenir Next', Avenir, 'Helvetica Neue', 'Lato', 'Segoe UI', Helvetica, Arial, sans-serif; 89 | overflow: hidden; 90 | margin: 0; 91 | color: #cecece; 92 | background: #2a2b30; 93 | -webkit-font-smoothing: antialiased; 94 | } 95 | 96 | body #main-content { 97 | margin-left: 330px; 98 | margin-right: 50px; 99 | height: 100%; 100 | } 101 | 102 | body #breadcrumb-nav { 103 | padding: 1em 0em; 104 | } 105 | 106 | body #breadcrumb-nav .breadcrumb-container{ 107 | display: inline-flex; 108 | width: 100%; 109 | background-color: #1c1d22; 110 | border-radius: 5px; 111 | } 112 | 113 | body #breadcrumb-nav .breadcrumb-container ol.breadcrumb { 114 | background-color: #1c1d22; 115 | margin: 0; 116 | } 117 | body #breadcrumb-nav .breadcrumb-container ol.breadcrumb .breadcrumb-item { 118 | padding-top: 10px; 119 | } 120 | 121 | body #breadcrumb-nav .breadcrumb-container .search-form-container { 122 | padding: 15px; 123 | } 124 | 125 | body #breadcrumb-nav .breadcrumb-container .search-form-container .search-form-dropdown { 126 | top: 0px !important; 127 | border-radius: 5px; 128 | left: 3px !important; 129 | min-width: 225px; 130 | } 131 | 132 | body #sidenav .nav-item { 133 | display: inline-block; 134 | width: 105%; 135 | padding: 5px 20px; 136 | background: #58595d; 137 | margin: 10px 0px; 138 | border-radius: 5px 0px 0px 10px; 139 | position: relative; 140 | word-break: break-word; 141 | } 142 | 143 | body #sidenav .nav-item:after { 144 | content: ''; 145 | position: absolute; 146 | bottom: -10px; 147 | right: 4px; 148 | transform: rotate(45deg); 149 | border: 10px solid transparent; 150 | border-right-color: #58595d; 151 | } 152 | 153 | #PlayBooks .clickable:hover { 154 | cursor: pointer; 155 | } 156 | 157 | 158 | 159 | #main-container .pb-tile { 160 | display: block; 161 | width: 200px; 162 | height: 200px; 163 | margin: 10px; 164 | border-radius: 5px; 165 | background: #1c1d22; 166 | float: left; 167 | position: relative; 168 | } 169 | 170 | #main-container .tile-main { 171 | height: calc(200px - 25px); 172 | display: table-cell; 173 | vertical-align: middle; 174 | width: inherit; 175 | text-align: center; 176 | word-break: break-word; 177 | } 178 | 179 | #main-container .tile-main .input-group { 180 | display: inline-block; 181 | } 182 | 183 | #main-container .tile-main .input-group input { 184 | width: 100%; 185 | border-radius: 0px 0px 5px 5px; 186 | } 187 | 188 | #main-container .tile-main .input-group .help-text { 189 | text-align: left; 190 | } 191 | 192 | form .help-text { 193 | padding: 0px 10px; 194 | } 195 | 196 | #main-container .tile-footer { 197 | position: absolute; 198 | bottom: 0; 199 | width: 100%; 200 | padding: 10px; 201 | display: inline-block; 202 | height: 50px; 203 | } 204 | 205 | #main-container .tile-footer input { 206 | padding: 2px 10px; 207 | } 208 | 209 | 210 | 211 | .modal-container .modal { 212 | z-index: 99999; 213 | } 214 | 215 | 216 | 217 | body .main-container{ 218 | height: 80%; 219 | overflow: auto; 220 | } 221 | 222 | body .tile { 223 | display: inline-block; 224 | width: 200px; 225 | height: 200px; 226 | margin: 10px; 227 | border-radius: 5px; 228 | background: #1c1d22; 229 | float: left; 230 | } 231 | 232 | body .codehilite { 233 | background: #f8f8f8; 234 | padding: 5px 5px 5px 5px; 235 | margin-bottom: 5px; 236 | border: 1px solid darkgrey; 237 | background-color: lightgray; 238 | } 239 | 240 | .codehilite pre { 241 | margin: 0; 242 | background-color: lightgray; 243 | } 244 | 245 | #pb-main pre{ 246 | background-color: lightgray; 247 | } 248 | 249 | #edit-page-modal .modal-content { 250 | display: inline-flex; 251 | } 252 | 253 | /** 254 | -- Side Navigation 255 | -- 256 | -- 257 | **/ 258 | 259 | body #side-content { 260 | position: fixed; 261 | top: 0; 262 | left: 0; 263 | width: 300px; 264 | background: #1c1d22; 265 | height: 100%; 266 | } 267 | 268 | body #side-content #sidenav { 269 | overflow-x: overlay; 270 | height: 80%; /* Height for scrolling */ 271 | width: 110%; /* For scroll not to overlap with content*/ 272 | } 273 | 274 | #side-content .nav-menu { 275 | width: 300px; /* re-enforce the width of the side content*/ 276 | } 277 | 278 | body #side-content #header { 279 | min-height: 100px; 280 | } 281 | 282 | body #side-content #header #header-logo{ 283 | padding: 1em 0 0 0; 284 | text-align: center; 285 | color: #3b3d4a; 286 | background: #1c1d22; 287 | } 288 | 289 | body #side-content #header #logo { 290 | width: 60%; 291 | margin: auto; 292 | margin-bottom: 10px; 293 | } 294 | 295 | 296 | body #header #user .username { 297 | color: #cecece; 298 | } 299 | body #header #user .logout { 300 | font-size: 85%; 301 | margin-left: 15px; 302 | } 303 | 304 | body #sidenav .nav-item:hover { 305 | background: #303131; 306 | } 307 | 308 | #sidenav .section-item { 309 | border-radius: 0px; 310 | display: inline-block; 311 | width: 100%; 312 | padding: 5px 20px; 313 | background: #58595d; 314 | margin: 10px 0px; 315 | position: relative; 316 | } 317 | 318 | #sidenav .btn { 319 | padding: 0px 10px; 320 | float: right; 321 | margin-left: 5px; 322 | } 323 | 324 | #sidenav .new-section { 325 | display: inline-block; 326 | width: 100%; 327 | border-bottom: 3px solid black; 328 | padding: 15px 0px; 329 | border-top: 3px solid black; 330 | } 331 | 332 | #sidenav .new-section .btn { 333 | margin-right: 10px; 334 | } 335 | 336 | .nav-icon.icon-draggable { 337 | margin-left: -10px; 338 | margin-right: 10px; 339 | } 340 | 341 | /** -- Form Fields -- **/ 342 | 343 | /** 344 | -- Form Fields 345 | -- 346 | -- 347 | **/ 348 | 349 | .edit-modal.modal-dialog { 350 | min-width: 50%; 351 | } 352 | 353 | .edit-modal .modal-content .page-add-module.add-existing { 354 | border-bottom: 3px solid #151618; 355 | } 356 | 357 | .modal-container .page-add-module { 358 | display: inline-flex; 359 | width: 100%; 360 | } 361 | 362 | .input-group-fieldset { 363 | width: 100%; 364 | display: inline-flex; 365 | } 366 | 367 | .error-message { 368 | color: #a90909; 369 | } 370 | 371 | .input-group-fieldset .errorlist li { 372 | display: block; 373 | } 374 | 375 | .btn-page-source { 376 | border: 1px solid; */ 377 | background: #e9ecef; 378 | padding: 5px 25px; 379 | /* box-shadow: 0 2px 6px rgba(0,0,0,0.25), 0 2px 2px rgba(0,0,0,0.22); */ 380 | border-radius: 3px; 381 | box-shadow: rgba(0, 0, 0, 0.2) 0px 0px 0px; 382 | background: rgb(35, 49, 66); 383 | color: #ccc; 384 | border: 1px solid rgb(35, 49, 66); 385 | /* background: transparent; */ 386 | border: 1px solid #ced4da; 387 | box-shadow: rgb(255, 255, 255) 0px 1px 0px 0px inset; 388 | background: linear-gradient(rgb(230, 230, 230) 5%, rgb(233, 236, 239) 100%) rgb(237, 237, 237); 389 | border-radius: 3px; 390 | border: 1px solid rgb(220, 220, 220); 391 | /* display: inline-block; */ 392 | cursor: pointer; 393 | color: rgb(119, 119, 119); 394 | /* font-family: Arial; */ 395 | font-size: 15px; 396 | font-weight: 550; 397 | padding: 6px 24px; 398 | text-decoration: none; 399 | text-shadow: rgb(255, 255, 255) 0px 1px 0px; 400 | background: linear-gradient(rgb(233, 236, 239) 5%, rgb(233, 236, 239) 100%) rgb(237, 237, 237); 401 | display: block; 402 | } 403 | 404 | .btn-page-source.active { 405 | box-shadow: inset 0 5px 5px rgba(0,0,0,.125); 406 | color: black; 407 | } 408 | 409 | .btn-page-source:hover { 410 | box-shadow: inset 0 5px 5px rgba(0,0,0,.125); 411 | } 412 | 413 | /** -- Form Fields -- **/ 414 | 415 | /** 416 | -- Page Content Edit 417 | -- 418 | -- 419 | **/ 420 | 421 | #page-content-edit { 422 | width: calc(100% - 420px); 423 | } 424 | 425 | #page-content-edit .label { 426 | font-weight: bold; 427 | margin-right: 5px; 428 | } 429 | 430 | #page-content-edit .page-update-status { 431 | display: inline-block; 432 | } 433 | 434 | #page-content-edit .page-update-status .notifications { 435 | margin-right: 10px; 436 | font-weight: normal; 437 | } 438 | 439 | #page-content-edit .page-update-status .notifications .status-update { 440 | color: #ecec85; 441 | } 442 | 443 | #page-content-edit .page-update-status .notifications .status-notmodified { 444 | color: #0bca0b; 445 | } 446 | 447 | #page-content-edit .page-update-status .notifications .status-unable { 448 | color: #9e9e9e; 449 | } 450 | 451 | #page-content-edit .page-update-status .btn { 452 | line-height: 1.0; 453 | } 454 | 455 | #page-content-edit .page-edit-controls .btn-edit { 456 | background-color: #49525a; 457 | } 458 | 459 | #page-content-edit .page-edit-controls .btn-edit:hover { 460 | background-color: #72808c; 461 | } 462 | 463 | #page-content-edit .page-edit-controls, #page-content-edit .page-update-controls { 464 | display: inline-block; 465 | margin: 5px 0px; 466 | width: 100%; 467 | } 468 | 469 | #page-content-edit #pb-controls { 470 | width: 100%; 471 | display: inline-block; 472 | } 473 | 474 | #page-content-edit .page-edit-controls button, #page-content-edit .page-update-controls button { 475 | margin-right: 10px; 476 | } 477 | 478 | #page-content-edit .page-content.content-control-fields { 479 | display: inline-flex; 480 | } 481 | 482 | #page-content-edit .page-content.content-control-fields .input-group { 483 | width: fit-content; 484 | float: left; 485 | margin-right: 10px; 486 | } 487 | 488 | #page-content-edit .contenteditable { 489 | color: #212529; 490 | } 491 | 492 | .toggle.btn { 493 | height: 100% !important; 494 | } 495 | /** -- Page Content Edit -- **/ 496 | 497 | 498 | /** 499 | -- Playbook Overview 500 | -- 501 | -- 502 | **/ 503 | 504 | .playbook-overview .attribute { 505 | margin-right: 10px; 506 | } 507 | 508 | .playbook-overview li { 509 | display: inline-block; 510 | } 511 | 512 | /** -- Playbook Overview -- **/ 513 | 514 | 515 | /** 516 | -- Prefetch Container 517 | -- 518 | -- 519 | **/ 520 | 521 | body #prefetch-container { 522 | position: absolute; 523 | height: 90%; 524 | width: 410px; 525 | top: 30px; 526 | right: 0px; 527 | overflow: auto; 528 | font-size: 70%; 529 | border: 2px solid #cecece; 530 | padding: 15px 15px; 531 | background-color: #131111; 532 | border-right: none; 533 | z-index: 9999; 534 | } 535 | 536 | #prefetch-container .close-container { 537 | color: white; 538 | } 539 | 540 | #prefetch-container code { 541 | color: #cecece; 542 | } 543 | #prefetch-container .codehilite { 544 | background-color: #a29f9f; 545 | } 546 | #prefetch-container .codehilite pre { 547 | background-color: #a29f9f; 548 | } 549 | #prefetch-container h1{ font-size: 2em; } 550 | #prefetch-container h2{ font-size: 1.5em; } 551 | #prefetch-container h3{ font-size: 1em; } 552 | #prefetch-container h4{ font-size: 0.8em; } 553 | 554 | /** -- Prefetch Container -- **/ 555 | 556 | /** 557 | -- Bootstrap Additions 558 | -- 559 | -- 560 | **/ 561 | 562 | .btn-default:hover, .btn-default:focus, .btn-default:active, .btn-default.active, .open>.dropdown-toggle.btn-default { 563 | color: #333; 564 | background-color: #e6e6e6; 565 | border-color: #adadad; 566 | box-shadow: inset 0 3px 5px rgba(0,0,0,.125) 567 | } 568 | .toggle-handle.btn-default { 569 | color: #333; 570 | background-color: #e9ecef; 571 | border-color: #ccc; 572 | border: 0.5px solid lightgray; 573 | } 574 | /** -- Bootstrap Additions -- **/ -------------------------------------------------------------------------------- /App/PlayBooksApp/templates/_edit_page.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /App/PlayBooksApp/templates/_page_content.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | {% if page.check_updates %} 9 |
10 | Update Status: 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | {% else %} 21 | {% if page.can_be_updated %} 22 | Update Status: 23 | 24 | Update Check has been disabled 25 | 26 | {% else %} 27 | Update Status: 28 | 29 | Source type {{page.source_type}} can not be updated 30 | 31 | {% endif %} 32 | {% endif %} 33 |
34 |
35 |
36 | 39 | 40 | 43 | 46 |
47 |
48 |
49 | 90 |
91 | 92 | 93 | {% csrf_token %} 94 | {{ form.text_source }} 95 |
96 | 101 |
102 | 103 |
104 | {% if html_page %} 105 | {{ html_page | safe }} 106 | {% endif %} 107 |
108 |
109 |
-------------------------------------------------------------------------------- /App/PlayBooksApp/templates/_search_results.html: -------------------------------------------------------------------------------- 1 | {% for page in pages %} 2 | {% for playbook in page.playbooks %} 3 | {{ page.title }} 4 | {% endfor %} 5 | {% endfor %} -------------------------------------------------------------------------------- /App/PlayBooksApp/templates/_select_server_file.html: -------------------------------------------------------------------------------- 1 | {% for v in optgroups %} 2 | 3 | {% for v2 in v.1 %} 4 | 5 | {% endfor %} 6 | 7 | {% endfor %} -------------------------------------------------------------------------------- /App/PlayBooksApp/templates/base.html: -------------------------------------------------------------------------------- 1 | {% load i18n static %} 2 | {% get_current_language as LANGUAGE_CODE %}{% get_current_language_bidi as LANGUAGE_BIDI %} 3 | 4 | 5 | {% block title %}{% endblock %} 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | {% block extrastyle %}{% endblock %} 20 | {% block extrahead %}{% endblock %} 21 | {% block blockbots %}{% endblock %} 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | {% if user.is_authenticated %} 30 |
31 |
32 | 45 | 46 |
47 | 50 |
51 |
52 | 53 | 54 |
55 | 74 |
75 |
76 | {% block main %}{% endblock %} 77 |
78 |
79 |
80 | 81 | 87 |
88 | 89 | {% endif %} 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | -------------------------------------------------------------------------------- /App/PlayBooksApp/templates/index.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block sidebar-playbooks %} 4 | 9 | {% endblock %} 10 | 11 | 12 | {% block main %} 13 |
14 | 52 |
53 | {% endblock %} 54 | -------------------------------------------------------------------------------- /App/PlayBooksApp/templates/playbook.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block breadcrumb %} 4 | 5 | 6 | {% endblock %} 7 | 8 | {% block sidebar-playbooks %} 9 |
10 | 50 |
51 | {% endblock %} 52 | 53 | 54 | {% block main %} 55 |
56 |
57 | {% for section in playbook.sections.all %} 58 | 67 | {% endfor %} 68 |
69 |
70 | {% endblock %} -------------------------------------------------------------------------------- /App/PlayBooksApp/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /App/PlayBooksApp/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from . import views 4 | 5 | app_name = '' ## Default App 6 | urlpatterns = [ 7 | path('', views.index, name='index'), 8 | path('pb//section//create', views.addPage, name="addPage"), 9 | path('pb//section//addExisting', views.addExistingPage, name="addExistingPage"), 10 | path('pb/', views.playbook, name="playbook"), 11 | path('pb//delete', views.deletePlaybook, name="deletePlaybook"), 12 | path('pb//addSection', views.addSection, name="addSection"), 13 | path('pb/section//updatePagePosition', views.updatePagePosition, name="updatePagePosition"), 14 | path('pb/section//delete', views.deleteSection, name="deleteSection"), 15 | path('pb/section//page//delete', views.deletePage, name="deletePage"), 16 | path('pb/page/', views.pageContent, name="pageContent"), 17 | path('pb/page//new', views.newPage, name="newPage"), 18 | path('pb/page//edit/', views.editPage, name="editPage"), 19 | path('pb/page//update/', views.updatePage, name="updatePage"), 20 | path('api/pb/prefetch', views.apiPrefetchSource, name="apiPrefetchSource"), 21 | path('api/pb/serverFiles', views.apiServerFileOptions, name="apiServerFileOptions"), 22 | path('api/pb/updateStatus/', views.apiPageUpateStatus, name="apiPageUpateStatus"), 23 | path('api/pb/updatePage/', views.apiUpdatePage, name="apiUpdatePage"), 24 | path('api/pb/prefetchUpdatePage/', views.apiPrefetchUpdatePage, name="apiPrefetchUpdatePage"), 25 | path('api/pb/pageTitle/', views.apiGetPageTitle, name="apiGetPageTitle"), 26 | path('api/search/', views.apiSearchPlaybooks, name="apiSearchPlaybooks"), 27 | ] 28 | -------------------------------------------------------------------------------- /App/PlayBooksApp/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render, redirect, get_object_or_404 2 | from django.http import HttpResponse, JsonResponse, Http404 3 | from django.views.decorators.csrf import csrf_protect, csrf_exempt 4 | from django.contrib.auth.decorators import login_required 5 | 6 | from PlayBooksApp.models import Playbook, PlaybookSection, SectionContent, Playpage 7 | from .forms import PlaypageForm, TEXTPLaypageForm, PlaybookSectionForm, PlaybookAddPageForm, PlaybookForm 8 | 9 | import copy 10 | import json 11 | 12 | from .request import URLRequester 13 | from .markdown import MarkdownParser 14 | from .source_resolver import SourceResolver 15 | 16 | 17 | @login_required 18 | @csrf_protect 19 | def index(request): 20 | if( request.POST ): 21 | form = PlaybookForm(request.user, request.POST) 22 | if( form.is_valid() ): 23 | form.save() 24 | return redirect('index') 25 | else: 26 | form = PlaybookForm(request.user) 27 | 28 | playbooks = Playbook.user_all(request.user) 29 | context = { 30 | 'playbooks': playbooks, 31 | 'form': form 32 | } 33 | return render(request, 'index.html', context) 34 | 35 | @login_required 36 | @csrf_protect 37 | def deletePlaybook(request, pbID): 38 | if( request.POST ): 39 | playbook = get_object_or_404(Playbook, pk=pbID) 40 | if( playbook.can_delete(request.user) ): 41 | for section in playbook.sections.all(): 42 | deleteSection(request, section.id) 43 | playbook.delete() 44 | else: 45 | return HttpResponse(status=403) 46 | 47 | return redirect('index') 48 | 49 | @login_required 50 | @csrf_protect 51 | def playbook(request, pbID): 52 | playbook = get_object_or_404(Playbook, pk=pbID) 53 | sections = playbook.user_sections(request.user) 54 | section_form = PlaybookSectionForm(request.user) 55 | context = { 56 | 'playbook': playbook, 57 | 'sections': sections, 58 | 'section_form': section_form 59 | } 60 | 61 | 62 | return render(request, 'playbook.html', context) 63 | 64 | @login_required 65 | @csrf_protect 66 | def editPage(request, pageID, sectionID): 67 | page = get_object_or_404(Playpage, pk=pageID) 68 | section = PlaybookSection.objects.get(pk=sectionID) 69 | form = PlaypageForm(request.user, instance=page) 70 | context = { 'page': page , 'form': form, 'section': section} 71 | 72 | return render(request, '_edit_page.html', context) 73 | 74 | @login_required 75 | @csrf_protect 76 | def pageContent(request, pageID): 77 | page = get_object_or_404(Playpage, pk=pageID) 78 | if( page.can_access(request.user) ): 79 | ## make a deep copy, the real page object should not be changed 80 | page_copy = copy.deepcopy(page) 81 | if( request.POST ): 82 | ## EDIT page content 83 | form = TEXTPLaypageForm(request.user, request.POST, request.FILES, instance=page_copy, auto_id='textform_%s') 84 | ## Most effective way to clean form data 85 | if( form.is_valid() ): 86 | ## only update the offline store 87 | page.offline_store = form.instance.offline_store 88 | page.title = form.instance.title 89 | page.check_updates = form.instance.check_updates 90 | page.save() 91 | else: 92 | return HttpResponse(status=417) 93 | else: 94 | form = TEXTPLaypageForm(request.user, instance=page_copy, auto_id='textform_%s') 95 | 96 | parser = MarkdownParser() 97 | md_content = page.offline_store 98 | html_page = parser.parseMD(md_content) if md_content else '' 99 | 100 | context = { 101 | 'form': form, 102 | 'html_page': html_page, 103 | 'page': page 104 | } 105 | 106 | return render(request, '_page_content.html', context) 107 | else: 108 | return HttpResponse( status=403 ) 109 | 110 | @login_required 111 | @csrf_protect 112 | def newPage(request, sectionID): 113 | section = PlaybookSection.objects.get(pk=sectionID) 114 | form = PlaypageForm(request.user) 115 | ## Only Add Existing Page Form if there are accessible pages 116 | add_page_form = False 117 | if( Playpage.accessible_pages(request.user) ): 118 | add_page_form = PlaybookAddPageForm(request.user) 119 | context = { 120 | 'form': form, 121 | 'add_page_form': add_page_form, 122 | 'section': section 123 | } 124 | 125 | return render(request, '_edit_page.html', context) 126 | 127 | @login_required 128 | @csrf_protect 129 | def updatePage(request, pageID, sectionID): 130 | 131 | ## Only POST Requests 132 | playpage = get_object_or_404(Playpage, pk=pageID) 133 | if( playpage.can_access(request.user) ): 134 | if( request.POST ): 135 | form = PlaypageForm(request.user, request.POST, request.FILES, instance=playpage) 136 | if( form.is_valid() ): 137 | playpage = form.save() 138 | else: 139 | section = PlaybookSection.objects.get(pk=sectionID) 140 | context = { 'page': playpage , 'form': form, 'section': section} 141 | return render(request, '_edit_page.html', context, status=417) 142 | 143 | return HttpResponse( status=200 ) 144 | else: 145 | return HttpResponse( status=403 ) 146 | 147 | @login_required 148 | @csrf_exempt 149 | def updatePagePosition(request, sectionID): 150 | if( request.is_ajax() ): 151 | try: 152 | data = json.loads(request.body) 153 | for dataset in data['positions']: 154 | position = dataset['position'] 155 | pageID = dataset['page'] 156 | page = get_object_or_404(Playpage, pk=pageID) 157 | if( page.can_change_position(request.user) ): 158 | page.set_position(position, sectionID) 159 | except: 160 | return HttpResponse( status=500 ) 161 | else: 162 | return HttpResponse( status=200 ) 163 | 164 | @login_required 165 | @csrf_protect 166 | def deletePage(request, sectionID, pageID): 167 | page = get_object_or_404(Playpage, pk=pageID) 168 | if( page.can_delete(request.user) ): 169 | try: 170 | ## if not existing in other sections delete page 171 | if ( page.sections.all().count() <= 1 ): 172 | page.delete() 173 | return HttpResponse( status=200 ) 174 | else: 175 | ## Delete page only from section 176 | sectionContent = SectionContent.objects.filter(playpage=pageID, section=sectionID) 177 | sectionContent.delete() 178 | return HttpResponse( status=200 ) 179 | except: 180 | return HttpResponse( status=500 ) 181 | else: 182 | return HttpResponse( status=403 ) 183 | 184 | @login_required 185 | @csrf_protect 186 | def addPage(request, pbID, sectionID): 187 | form = PlaypageForm(request.user, request.POST, request.FILES) 188 | section = PlaybookSection.objects.prefetch_related().get(pk=sectionID) 189 | 190 | if( form.is_valid() ): 191 | ## all valid 192 | playpage = form.save() 193 | section.append_page(playpage) 194 | else: 195 | ## not valid form 196 | context = { 'form': form, 'section': section } 197 | return render(request, '_edit_page.html', context, status=417) 198 | 199 | return HttpResponse( status=200 ) 200 | 201 | @login_required 202 | @csrf_protect 203 | def addExistingPage(request, pbID, sectionID): 204 | section = get_object_or_404(PlaybookSection, pk=sectionID) 205 | form = PlaybookAddPageForm(request.user, request.POST, request.FILES) 206 | 207 | if( form.is_valid() ): 208 | ## get referenced page 209 | pageID = form['page'].data 210 | page = get_object_or_404(Playpage, pk=pageID) 211 | ## append page 212 | section.append_page( page ) 213 | 214 | else: 215 | ## not valid form 216 | context = { 'form': form, 'section': section } 217 | return render(request, '_edit_page.html', context, status=417) 218 | 219 | return HttpResponse( status=200 ) 220 | 221 | @login_required 222 | @csrf_protect 223 | def addSection(request, pbID): 224 | playbook = get_object_or_404(Playbook, pk=pbID) 225 | section = PlaybookSection(playbook=playbook) 226 | form = PlaybookSectionForm(request.user, request.POST, instance=section) 227 | last_section = playbook.sections.last() 228 | if( last_section and last_section.section_position ): 229 | ## append to sections 230 | last_position = last_section.section_position 231 | section_position = last_position + 1 232 | else: 233 | ## No sections so far 234 | section_position = 0 235 | ## check valid 236 | if( form.is_valid() ): 237 | form.instance.section_position = section_position 238 | section = form.save() 239 | return HttpResponse( status=200 ) 240 | else: 241 | return HttpResponse( status=417 ) 242 | 243 | @login_required 244 | @csrf_protect 245 | def deleteSection(request, sectionID): 246 | section = get_object_or_404(PlaybookSection, pk=sectionID) 247 | if( section.can_delete(request.user) ): 248 | section_pages = section.pages.all() 249 | ## check if pages exits on other sections 250 | for page in section_pages: 251 | ## if not existing in other sections delete page as well 252 | if ( page.sections.all().count() <= 1 ): 253 | if( page.can_delete(request.user) ): 254 | page.delete() 255 | ## delete section 256 | section.delete() 257 | return HttpResponse( status=200 ) 258 | else: 259 | return HttpResponse( status=403 ) 260 | 261 | @login_required 262 | def apiServerFileOptions(request): 263 | if( request.POST['page_id'] ): 264 | pageID = request.POST.get('page_id') 265 | page = Playpage.objects.get( pk=pageID ) 266 | if( page.can_access(request.user) ): 267 | form = PlaypageForm(request.user, request.POST, request.FILES, instance=page) 268 | else: 269 | return HttpResponse( status=403 ) 270 | else: 271 | form = PlaypageForm(request.user, request.POST, request.FILES) 272 | 273 | form.is_valid() ## seems like the 'cleanest' option to call form.clean() 274 | optgroups = form.get_included_folder_path_options() 275 | context = { 'optgroups': optgroups } 276 | return render(request, '_select_server_file.html', context) 277 | 278 | @login_required 279 | def apiPrefetchSource(request): 280 | if( request.POST['page_id'] ): 281 | pageID = request.POST.get('page_id') 282 | page = Playpage.objects.get( pk=pageID ) 283 | if( page.can_access(request.user) ): 284 | form = PlaypageForm(request.user, request.POST, request.FILES, instance=page) 285 | else: 286 | return HttpResponse( status=403 ) 287 | else: 288 | form = PlaypageForm(request.user, request.POST, request.FILES) 289 | ## Clean form data 290 | source_type = form['source_type'].data 291 | valid_source = form.validate_source_type(source_type) 292 | 293 | if( valid_source ): 294 | form.is_valid() ## seems like the 'cleanest' option to call form.clean() 295 | md_content = form.instance.offline_store 296 | parser = MarkdownParser() 297 | html = parser.parseMD( md_content ) 298 | return HttpResponse( html, status=200 ) 299 | else: 300 | return JsonResponse("Error", safe=False) 301 | 302 | @login_required 303 | def apiGetPageTitle(request, pageID): 304 | playpage = get_object_or_404(Playpage, pk=pageID) 305 | if( playpage.can_access(request.user) ): 306 | if( playpage ): 307 | page_title = playpage.title 308 | response = {'title': page_title} 309 | return JsonResponse( response, status=200 ) 310 | else: 311 | response = {'title': ''} 312 | return JsonResponse( response, status=404 ) 313 | else: 314 | return HttpResponse( status=403 ) 315 | 316 | @login_required 317 | def apiPageUpateStatus(request, pageID): 318 | page = get_object_or_404(Playpage, pk=pageID) 319 | if( page.can_access(request.user) ): 320 | resolver = SourceResolver(page) 321 | updated_offline_store = resolver.resolve_type() 322 | if( updated_offline_store != page.offline_store ): 323 | return HttpResponse( status=200 ) 324 | else: 325 | return HttpResponse( status=304 ) 326 | else: 327 | return HttpResponse( status=403 ) 328 | 329 | @login_required 330 | def apiUpdatePage(request, pageID): 331 | page = get_object_or_404(Playpage, pk=pageID) 332 | if( page.can_access(request.user) ): 333 | resolver = SourceResolver(page) 334 | updated_offline_store = resolver.resolve_type() 335 | if( updated_offline_store ): 336 | page.offline_store = updated_offline_store 337 | page.save() 338 | return HttpResponse( status=200 ) 339 | else: 340 | return HttpResponse( status=500 ) 341 | else: 342 | return HttpResponse( status=403 ) 343 | 344 | @login_required 345 | def apiPrefetchUpdatePage(request, pageID): 346 | page = get_object_or_404(Playpage, pk=pageID) 347 | if( page.can_access(request.user) ): 348 | resolver = SourceResolver(page) 349 | updated_offline_store = resolver.resolve_type() 350 | if( updated_offline_store ): 351 | md_content = updated_offline_store 352 | parser = MarkdownParser() 353 | html = parser.parseMD( md_content ) 354 | return HttpResponse( html, status=200 ) 355 | else: 356 | return HttpResponse( status=500 ) 357 | else: 358 | return HttpResponse( status=403 ) 359 | 360 | @login_required 361 | def apiSearchPlaybooks(request): 362 | searchq = request.POST['search'] 363 | results_all = Playpage.objects.filter(offline_store__contains=searchq) 364 | results_access = [ page for page in results_all if page.can_access(request.user) ] 365 | context = { 'pages': results_access } 366 | return render(request, '_search_results.html', context) 367 | -------------------------------------------------------------------------------- /App/PlayBooksWeb/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/csandker/Playbooks/1da352e6b584fe51fd3e758e5e2a404ebf299762/App/PlayBooksWeb/__init__.py -------------------------------------------------------------------------------- /App/PlayBooksWeb/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class PlayBooksWebConfig(AppConfig): 5 | name = 'PlayBooksWeb' 6 | -------------------------------------------------------------------------------- /App/PlayBooksWeb/asgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | ASGI config for PlayBooksWeb project. 3 | 4 | It exposes the ASGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.0/howto/deployment/asgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.asgi import get_asgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'PlayBooksWeb.settings') 15 | 16 | application = get_asgi_application() 17 | -------------------------------------------------------------------------------- /App/PlayBooksWeb/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | from django.forms import ModelForm 3 | from django.utils.translation import gettext_lazy as _ 4 | 5 | from .validators import validate_path_access 6 | from .models import IncludedFolder 7 | 8 | class IncludeFolderForm(ModelForm): 9 | class Meta: 10 | model = IncludedFolder 11 | fields = ['name', 'path', 'enabled'] 12 | help_texts = { 13 | 'name': _("Name of the folder. Can contain charachters, numbers, '-'' & '_'."), 14 | 'path': _("Absolute Path. Access to it will be validated automatically"), 15 | 'enabled': _("Path can be enabled and disabled dynamically"), 16 | } 17 | widgets={'path': forms.TextInput(attrs={'size': '80'})} 18 | -------------------------------------------------------------------------------- /App/PlayBooksWeb/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.0.4 on 2020-04-02 18:51 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | initial = True 9 | 10 | dependencies = [ 11 | ] 12 | 13 | operations = [ 14 | migrations.CreateModel( 15 | name='IncludedFolder', 16 | fields=[ 17 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 18 | ('name', models.CharField(max_length=500)), 19 | ('path', models.CharField(max_length=10000)), 20 | ('enabled', models.BooleanField(default=True)), 21 | ('created_at', models.DateTimeField(auto_now_add=True, null=True)), 22 | ('last_modified', models.DateTimeField(auto_now=True, null=True)), 23 | ], 24 | ), 25 | ] 26 | -------------------------------------------------------------------------------- /App/PlayBooksWeb/migrations/0002_auto_20200404_1311.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.0.4 on 2020-04-04 11:11 2 | 3 | import PlayBooksWeb.validators 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('PlayBooksWeb', '0001_initial'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name='includedfolder', 16 | name='path', 17 | field=models.CharField(max_length=10000, validators=[PlayBooksWeb.validators.validate_path_access]), 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /App/PlayBooksWeb/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/csandker/Playbooks/1da352e6b584fe51fd3e758e5e2a404ebf299762/App/PlayBooksWeb/migrations/__init__.py -------------------------------------------------------------------------------- /App/PlayBooksWeb/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.core.exceptions import ValidationError 3 | 4 | from .validators import validate_path_access, validate_file_access 5 | 6 | import os 7 | import re 8 | import base64 9 | 10 | class IncludedFolder(models.Model): 11 | name = models.CharField(max_length=500) 12 | path = models.CharField(max_length=10000, validators=[validate_path_access]) 13 | enabled = models.BooleanField(default=True) 14 | created_at = models.DateTimeField(auto_now_add=True, null=True, blank=True) 15 | last_modified = models.DateTimeField(auto_now=True, null=True, blank=True) 16 | 17 | @classmethod 18 | def list_allowed_files(self, folderID, allowedExtensions): 19 | folder = self.objects.get( pk=folderID ) 20 | path = folder.path 21 | allowedExtensionsTuple = () 22 | ## Include Uppercases 23 | for ext in allowedExtensions: 24 | allowedExtensionsTuple += (ext,) 25 | allowedExtensionsTuple += (ext.upper(),) 26 | 27 | ## add trailing / 28 | path += '/' if path.endswith('') else '' 29 | ## create dict of files in folders 30 | global_dict = {} 31 | for file_root_path, dirs, files in os.walk(path): 32 | for file in files: 33 | if( file.endswith( allowedExtensionsTuple ) ): 34 | ## cut off root path 35 | containing_dir = file_root_path.replace(path, '') 36 | containing_dir += '' if containing_dir.endswith('/') else '/' 37 | files_array = [] 38 | if( containing_dir in global_dict ): 39 | files_array = global_dict[containing_dir] 40 | else: 41 | global_dict[containing_dir] = files_array 42 | file_option_value = "%s%s" %(containing_dir, file) 43 | file_dict = { file_option_value: file } 44 | files_array.append( file_dict ) 45 | global_dict[containing_dir] = files_array 46 | 47 | global_options = self.dict_to_option_tuple(global_dict) 48 | return global_options 49 | 50 | @classmethod 51 | def dict_to_option_tuple(self, global_dict): 52 | dir_options = () 53 | for folder_name in global_dict: 54 | folder_option = (folder_name,) 55 | fileoptions = () 56 | files = global_dict[folder_name] 57 | for file_dict in files: 58 | for file_id in file_dict: 59 | name = file_dict[file_id] 60 | fileoption = (file_id, name) 61 | fileoptions += ((fileoption),) 62 | 63 | 64 | folder_option += ((fileoptions),) 65 | dir_options += ( folder_option, ) 66 | 67 | return ( dir_options ) 68 | 69 | 70 | @classmethod 71 | def get_img_full_path(self, imgSubPath, folderID=None, subpath=None): 72 | folder = self.objects.get( pk=folderID ) 73 | folder_path = folder.path 74 | folder_path += '' if folder_path.endswith('/') else '/' 75 | 76 | imgFullPath = os.path.abspath(os.path.join(folder_path, subpath, '..', imgSubPath)) 77 | return imgFullPath 78 | 79 | @classmethod 80 | def valid_file_path(self, path): 81 | try: 82 | validate_file_access(path) 83 | except: 84 | return False 85 | else: 86 | return True 87 | 88 | @classmethod 89 | def path_validation(self, imgSubPath, folderID=None, subpath=None): 90 | imgFullPath = self.get_img_full_path(imgSubPath, folderID=folderID, subpath=subpath) 91 | return self.valid_file_path(imgFullPath) 92 | 93 | @classmethod 94 | def path_processing(self, imgSubPath, folderID=None, subpath=None): 95 | imgFullPath = self.get_img_full_path(imgSubPath, folderID=folderID, subpath=subpath) 96 | fdImg = os.open(imgFullPath, os.O_RDONLY) 97 | bufferImg = os.read(fdImg, os.path.getsize(fdImg)) 98 | base64Img = base64.b64encode(bufferImg).decode('ascii') 99 | os.close(fdImg) 100 | 101 | if( base64Img ): 102 | return base64Img 103 | else: 104 | return False 105 | 106 | @classmethod 107 | def get_file_content(self, folderID, subpath): 108 | folder = self.objects.get( pk=folderID ) 109 | path = folder.path 110 | path += '' if path.endswith('/') else '/' 111 | fullpath = '%s%s' %(path, subpath) 112 | fd = os.open(fullpath, os.O_RDONLY) 113 | buffer = os.read(fd, os.path.getsize(fd)) 114 | content = buffer.decode('utf-8', errors='ignore') 115 | os.close(fd) 116 | 117 | return content 118 | -------------------------------------------------------------------------------- /App/PlayBooksWeb/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for PlayBooksWeb project. 3 | 4 | Generated by 'django-admin startproject' using Django 3.0.4. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.0/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/3.0/ref/settings/ 11 | """ 12 | 13 | import os 14 | import json 15 | 16 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 17 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 18 | 19 | ## settings 20 | try: 21 | fh = open(os.path.join(BASE_DIR, 'settings.json'), 'r') 22 | settings = json.load(fh) 23 | fh.close() 24 | except: 25 | settings = {} 26 | 27 | # Quick-start development settings - unsuitable for production 28 | # See https://docs.djangoproject.com/en/3.0/howto/deployment/checklist/ 29 | 30 | # SECURITY WARNING: keep the secret key used in production secret! 31 | SECRET_KEY = settings['SECRET_KEY'] if 'SECRET_KEY' in settings else "qe3x*y@pp1h_r#(rvqlc=-0l+y&l5^u%jw&*p_13wb4=z!6))m" 32 | 33 | 34 | # SECURITY WARNING: don't run with debug turned on in production! 35 | if( 'PROD_ENV' in settings and settings['PROD_ENV'] ): 36 | DEBUG = False 37 | else: 38 | DEBUG = True 39 | 40 | ALLOWED_HOSTS = settings['ALLOWED_HOSTS'] if 'ALLOWED_HOSTS' in settings else ['*'] 41 | 42 | # Application definition 43 | 44 | INSTALLED_APPS = [ 45 | 'PlayBooksApp.apps.PlayBooksAppConfig', 46 | 'PlayBooksWeb.apps.PlayBooksWebConfig', 47 | 'django.contrib.admin', 48 | 'django.contrib.auth', 49 | 'django.contrib.contenttypes', 50 | 'django.contrib.sessions', 51 | 'django.contrib.messages', 52 | 'django.contrib.staticfiles', 53 | ] 54 | 55 | MIDDLEWARE = [ 56 | 'django.middleware.security.SecurityMiddleware', 57 | 'django.contrib.sessions.middleware.SessionMiddleware', 58 | 'django.middleware.common.CommonMiddleware', 59 | 'django.middleware.csrf.CsrfViewMiddleware', 60 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 61 | 'django.contrib.messages.middleware.MessageMiddleware', 62 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 63 | ] 64 | 65 | ROOT_URLCONF = 'PlayBooksWeb.urls' 66 | 67 | TEMPLATES = [ 68 | { 69 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 70 | 'DIRS': [os.path.join(BASE_DIR, 'PlayBooksWeb','templates')], 71 | 'APP_DIRS': True, 72 | 'OPTIONS': { 73 | 'context_processors': [ 74 | 'django.template.context_processors.debug', 75 | 'django.template.context_processors.request', 76 | 'django.contrib.auth.context_processors.auth', 77 | 'django.contrib.messages.context_processors.messages', 78 | ], 79 | }, 80 | }, 81 | ] 82 | 83 | WSGI_APPLICATION = 'PlayBooksWeb.wsgi.application' 84 | 85 | 86 | # Database 87 | # https://docs.djangoproject.com/en/3.0/ref/settings/#databases 88 | SQLITE3_PATH = settings['SQLITE3_PATH'] if 'SQLITE3_PATH' in settings else os.path.join(BASE_DIR, 'db.sqlite3') 89 | SQLite3_DB = { 90 | 'ENGINE': 'django.db.backends.sqlite3', 91 | 'NAME': SQLITE3_PATH 92 | } 93 | 94 | DATABASES = { 95 | 'default': SQLite3_DB 96 | } 97 | 98 | 99 | # Password validation 100 | # https://docs.djangoproject.com/en/3.0/ref/settings/#auth-password-validators 101 | 102 | AUTH_PASSWORD_VALIDATORS = [ 103 | { 104 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 105 | }, 106 | { 107 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 108 | }, 109 | { 110 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 111 | }, 112 | { 113 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 114 | }, 115 | ] 116 | 117 | # Redirect to home URL after login (Default redirects to /accounts/profile/) 118 | LOGIN_REDIRECT_URL = '/' 119 | LOGIN_URL = '/auth/login' 120 | LOGOUT_REDIRECT_URL = '/auth/login' 121 | 122 | # Internationalization 123 | # https://docs.djangoproject.com/en/3.0/topics/i18n/ 124 | 125 | LANGUAGE_CODE = 'en-us' 126 | TIME_ZONE = os.environ.get('PLAYBOOKS_TIMEZONE', 'Europe/Berlin') 127 | USE_I18N = True 128 | USE_L10N = True 129 | USE_TZ = True 130 | 131 | 132 | # Static files (CSS, JavaScript, Images) 133 | # https://docs.djangoproject.com/en/3.0/howto/static-files/ 134 | 135 | STATIC_URL = '/static/' 136 | STATIC_ROOT=os.path.join(BASE_DIR, 'static/') 137 | STATICFILES_DIRS = ( 138 | 139 | # Put strings here, like "/home/html/static" or "C:/www/django/static". 140 | # Always use forward slashes, even on Windows. 141 | # Don't forget to use absolute paths, not relative paths. 142 | os.path.join(BASE_DIR, 'PlayBooksWeb','static'), 143 | ) -------------------------------------------------------------------------------- /App/PlayBooksWeb/static/css/login.css: -------------------------------------------------------------------------------- 1 | body{ 2 | background: #1c1d22; 3 | /* background: url(/static/img/Backgrnd.svg) no-repeat center; */ 4 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#1c1d22', endColorstr='#2a2b30', GradientType=0); 5 | background: -webkit-linear-gradient(to bottom, rgb(28, 29, 34) 50%, #2a2b30) !important; 6 | background: -moz-linear-gradient(to bottom, #1c1d22 50%, #2a2b30) !important; 7 | background: -ms-linear-gradient(to bottom, #1c1d22 50%, #2a2b30) !important; 8 | background: -o-linear-gradient(to bottom, #1c1d22 50%, #2a2b30) !important; 9 | background: linear-gradient(to bottom, #1c1d22 50%, #2a2b30) !important; 10 | color: white; 11 | } 12 | 13 | div.well{ 14 | height: 250px; 15 | } 16 | 17 | .Absolute-Center { 18 | margin: auto; 19 | position: absolute; 20 | top: 0; left: 0; bottom: 0; right: 0; 21 | } 22 | 23 | .Absolute-Center.is-Responsive { 24 | width: 50%; 25 | height: 50%; 26 | min-width: 200px; 27 | max-width: 400px; 28 | padding: 40px; 29 | } 30 | 31 | #logo-container{ 32 | margin: auto; 33 | margin-bottom: 10px; 34 | width:200px; 35 | height:30px; 36 | background-image: url(''); 37 | } 38 | 39 | /** 40 | Bootstrap Additions 41 | **/ 42 | 43 | .input-group { 44 | background: rgba(255, 255, 255, 0.15); 45 | display: flex; 46 | display: flex; 47 | display: -webkit-box; 48 | display: -moz-box; 49 | display: -ms-flexbox; 50 | display: -webkit-flex; 51 | margin-bottom: 20px; 52 | padding: 2px 10px 2px 0; 53 | border-radius: 35px; 54 | -webkit-border-radius: 35px; 55 | -moz-border-radius: 35px; 56 | -ms-border-radius: 35px; 57 | -o-border-radius: 35px; 58 | } 59 | 60 | span.input-icon{ 61 | background: transparent; 62 | border: none; 63 | flex: 1; 64 | -webkit-box-flex: 1; 65 | -moz-box-flex: 1; 66 | width: 20%; 67 | -webkit-flex: 1; 68 | -ms-flex: 1; 69 | color: #fff; 70 | background: transparent; 71 | line-height: 49px; 72 | text-align: right; 73 | } 74 | 75 | .input-group input { 76 | padding: 10px 0 10px 15px; 77 | font-size: 17px; 78 | font-weight: 300; 79 | color: #ddd; 80 | letter-spacing: 1px; 81 | border: none; 82 | background: transparent; 83 | box-sizing: border-box; 84 | font-family: 'Mukta', sans-serif; 85 | width: 100%; 86 | outline: none; 87 | 88 | display: block; 89 | height: calc(1.5em + .75rem + 2px); 90 | line-height: 1.5; 91 | transition: border-color .15s ease-in-out,box-shadow .15s ease-in-out; 92 | } 93 | 94 | .input-group input { 95 | position: relative; 96 | -ms-flex: 1 1 0%; 97 | flex: 1 1 0%; 98 | min-width: 0; 99 | margin-bottom: 0; 100 | } 101 | 102 | .submit-btn { 103 | background: #3a4c5f; 104 | border: none; 105 | color: #fff; 106 | padding: 11px 15px; 107 | text-transform: uppercase; 108 | font-family: 'Mukta', sans-serif; 109 | font-size: 16px; 110 | width: 100%; 111 | margin-top: 10px; 112 | letter-spacing: 2px; 113 | cursor: pointer; 114 | transition: 0.5s all; 115 | -webkit-transition: 0.5s all; 116 | -moz-transition: 0.5s all; 117 | -o-transition: 0.5s all; 118 | -ms-transition: 0.5s all; 119 | border-radius: 35px; 120 | -webkit-border-radius: 35px; 121 | -moz-border-radius: 35px; 122 | -ms-border-radius: 35px; 123 | -o-border-radius: 35px; 124 | } 125 | 126 | #logo-container { 127 | margin: 0px 15px 40px 33px; 128 | } 129 | 130 | .error { 131 | color: #e01a1a 132 | } -------------------------------------------------------------------------------- /App/PlayBooksWeb/static/external/css/bootstrap-select.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap-select v1.13.9 (https://developer.snapappointments.com/bootstrap-select) 3 | * 4 | * Copyright 2012-2019 SnapAppointments, LLC 5 | * Licensed under MIT (https://github.com/snapappointments/bootstrap-select/blob/master/LICENSE) 6 | */.bootstrap-select>select.bs-select-hidden,select.bs-select-hidden,select.selectpicker{display:none!important}.bootstrap-select{width:220px\0;vertical-align:middle}.bootstrap-select>.dropdown-toggle{position:relative;width:100%;text-align:right;white-space:nowrap;display:-webkit-inline-box;display:-webkit-inline-flex;display:-ms-inline-flexbox;display:inline-flex;-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:justify;-webkit-justify-content:space-between;-ms-flex-pack:justify;justify-content:space-between}.bootstrap-select>.dropdown-toggle:after{margin-top:-1px}.bootstrap-select>.dropdown-toggle.bs-placeholder,.bootstrap-select>.dropdown-toggle.bs-placeholder:active,.bootstrap-select>.dropdown-toggle.bs-placeholder:focus,.bootstrap-select>.dropdown-toggle.bs-placeholder:hover{color:#999}.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-danger,.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-danger:active,.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-danger:focus,.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-danger:hover,.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-dark,.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-dark:active,.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-dark:focus,.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-dark:hover,.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-info,.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-info:active,.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-info:focus,.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-info:hover,.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-primary,.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-primary:active,.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-primary:focus,.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-primary:hover,.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-secondary,.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-secondary:active,.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-secondary:focus,.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-secondary:hover,.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-success,.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-success:active,.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-success:focus,.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-success:hover{color:rgba(255,255,255,.5)}.bootstrap-select>select{position:absolute!important;bottom:0;left:50%;display:block!important;width:.5px!important;height:100%!important;padding:0!important;opacity:0!important;border:none;z-index:0!important}.bootstrap-select>select.mobile-device{top:0;left:0;display:block!important;width:100%!important;z-index:2!important}.bootstrap-select.is-invalid .dropdown-toggle,.error .bootstrap-select .dropdown-toggle,.has-error .bootstrap-select .dropdown-toggle,.was-validated .bootstrap-select .selectpicker:invalid+.dropdown-toggle{border-color:#b94a48}.bootstrap-select.is-valid .dropdown-toggle,.was-validated .bootstrap-select .selectpicker:valid+.dropdown-toggle{border-color:#28a745}.bootstrap-select.fit-width{width:auto!important}.bootstrap-select:not([class*=col-]):not([class*=form-control]):not(.input-group-btn){width:220px}.bootstrap-select .dropdown-toggle:focus,.bootstrap-select>select.mobile-device:focus+.dropdown-toggle{outline:thin dotted #333!important;outline:5px auto -webkit-focus-ring-color!important;outline-offset:-2px}.bootstrap-select.form-control{margin-bottom:0;padding:0;border:none}:not(.input-group)>.bootstrap-select.form-control:not([class*=col-]){width:100%}.bootstrap-select.form-control.input-group-btn{float:none;z-index:auto}.form-inline .bootstrap-select,.form-inline .bootstrap-select.form-control:not([class*=col-]){width:auto}.bootstrap-select:not(.input-group-btn),.bootstrap-select[class*=col-]{float:none;display:inline-block;margin-left:0}.bootstrap-select.dropdown-menu-right,.bootstrap-select[class*=col-].dropdown-menu-right,.row .bootstrap-select[class*=col-].dropdown-menu-right{float:right}.form-group .bootstrap-select,.form-horizontal .bootstrap-select,.form-inline .bootstrap-select{margin-bottom:0}.form-group-lg .bootstrap-select.form-control,.form-group-sm .bootstrap-select.form-control{padding:0}.form-group-lg .bootstrap-select.form-control .dropdown-toggle,.form-group-sm .bootstrap-select.form-control .dropdown-toggle{height:100%;font-size:inherit;line-height:inherit;border-radius:inherit}.bootstrap-select.form-control-lg .dropdown-toggle,.bootstrap-select.form-control-sm .dropdown-toggle{font-size:inherit;line-height:inherit;border-radius:inherit}.bootstrap-select.form-control-sm .dropdown-toggle{padding:.25rem .5rem}.bootstrap-select.form-control-lg .dropdown-toggle{padding:.5rem 1rem}.form-inline .bootstrap-select .form-control{width:100%}.bootstrap-select.disabled,.bootstrap-select>.disabled{cursor:not-allowed}.bootstrap-select.disabled:focus,.bootstrap-select>.disabled:focus{outline:0!important}.bootstrap-select.bs-container{position:absolute;top:0;left:0;height:0!important;padding:0!important}.bootstrap-select.bs-container .dropdown-menu{z-index:1060}.bootstrap-select .dropdown-toggle .filter-option{position:static;top:0;left:0;float:left;height:100%;width:100%;text-align:left;overflow:hidden;-webkit-box-flex:0;-webkit-flex:0 1 auto;-ms-flex:0 1 auto;flex:0 1 auto}.bs3.bootstrap-select .dropdown-toggle .filter-option{padding-right:inherit}.input-group .bs3-has-addon.bootstrap-select .dropdown-toggle .filter-option{position:absolute;padding-top:inherit;padding-bottom:inherit;padding-left:inherit;float:none}.input-group .bs3-has-addon.bootstrap-select .dropdown-toggle .filter-option .filter-option-inner{padding-right:inherit}.bootstrap-select .dropdown-toggle .filter-option-inner-inner{overflow:hidden}.bootstrap-select .dropdown-toggle .filter-expand{width:0!important;float:left;opacity:0!important;overflow:hidden}.bootstrap-select .dropdown-toggle .caret{position:absolute;top:50%;right:12px;margin-top:-2px;vertical-align:middle}.input-group .bootstrap-select.form-control .dropdown-toggle{border-radius:inherit}.bootstrap-select[class*=col-] .dropdown-toggle{width:100%}.bootstrap-select .dropdown-menu{min-width:100%;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.bootstrap-select .dropdown-menu>.inner:focus{outline:0!important}.bootstrap-select .dropdown-menu.inner{position:static;float:none;border:0;padding:0;margin:0;border-radius:0;-webkit-box-shadow:none;box-shadow:none}.bootstrap-select .dropdown-menu li{position:relative}.bootstrap-select .dropdown-menu li.active small{color:rgba(255,255,255,.5)!important}.bootstrap-select .dropdown-menu li.disabled a{cursor:not-allowed}.bootstrap-select .dropdown-menu li a{cursor:pointer;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.bootstrap-select .dropdown-menu li a.opt{position:relative;padding-left:2.25em}.bootstrap-select .dropdown-menu li a span.check-mark{display:none}.bootstrap-select .dropdown-menu li a span.text{display:inline-block}.bootstrap-select .dropdown-menu li small{padding-left:.5em}.bootstrap-select .dropdown-menu .notify{position:absolute;bottom:5px;width:96%;margin:0 2%;min-height:26px;padding:3px 5px;background:#f5f5f5;border:1px solid #e3e3e3;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.05);box-shadow:inset 0 1px 1px rgba(0,0,0,.05);pointer-events:none;opacity:.9;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.bootstrap-select .no-results{padding:3px;background:#f5f5f5;margin:0 5px;white-space:nowrap}.bootstrap-select.fit-width .dropdown-toggle .filter-option{position:static;display:inline;padding:0;width:auto}.bootstrap-select.fit-width .dropdown-toggle .filter-option-inner,.bootstrap-select.fit-width .dropdown-toggle .filter-option-inner-inner{display:inline}.bootstrap-select.fit-width .dropdown-toggle .bs-caret:before{content:'\00a0'}.bootstrap-select.fit-width .dropdown-toggle .caret{position:static;top:auto;margin-top:-1px}.bootstrap-select.show-tick .dropdown-menu .selected span.check-mark{position:absolute;display:inline-block;right:15px;top:5px}.bootstrap-select.show-tick .dropdown-menu li a span.text{margin-right:34px}.bootstrap-select .bs-ok-default:after{content:'';display:block;width:.5em;height:1em;border-style:solid;border-width:0 .26em .26em 0;-webkit-transform:rotate(45deg);-ms-transform:rotate(45deg);-o-transform:rotate(45deg);transform:rotate(45deg)}.bootstrap-select.show-menu-arrow.open>.dropdown-toggle,.bootstrap-select.show-menu-arrow.show>.dropdown-toggle{z-index:1061}.bootstrap-select.show-menu-arrow .dropdown-toggle .filter-option:before{content:'';border-left:7px solid transparent;border-right:7px solid transparent;border-bottom:7px solid rgba(204,204,204,.2);position:absolute;bottom:-4px;left:9px;display:none}.bootstrap-select.show-menu-arrow .dropdown-toggle .filter-option:after{content:'';border-left:6px solid transparent;border-right:6px solid transparent;border-bottom:6px solid #fff;position:absolute;bottom:-4px;left:10px;display:none}.bootstrap-select.show-menu-arrow.dropup .dropdown-toggle .filter-option:before{bottom:auto;top:-4px;border-top:7px solid rgba(204,204,204,.2);border-bottom:0}.bootstrap-select.show-menu-arrow.dropup .dropdown-toggle .filter-option:after{bottom:auto;top:-4px;border-top:6px solid #fff;border-bottom:0}.bootstrap-select.show-menu-arrow.pull-right .dropdown-toggle .filter-option:before{right:12px;left:auto}.bootstrap-select.show-menu-arrow.pull-right .dropdown-toggle .filter-option:after{right:13px;left:auto}.bootstrap-select.show-menu-arrow.open>.dropdown-toggle .filter-option:after,.bootstrap-select.show-menu-arrow.open>.dropdown-toggle .filter-option:before,.bootstrap-select.show-menu-arrow.show>.dropdown-toggle .filter-option:after,.bootstrap-select.show-menu-arrow.show>.dropdown-toggle .filter-option:before{display:block}.bs-actionsbox,.bs-donebutton,.bs-searchbox{padding:4px 8px}.bs-actionsbox{width:100%;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.bs-actionsbox .btn-group button{width:50%}.bs-donebutton{float:left;width:100%;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.bs-donebutton .btn-group button{width:100%}.bs-searchbox+.bs-actionsbox{padding:0 8px 4px}.bs-searchbox .form-control{margin-bottom:0;width:100%;float:none} -------------------------------------------------------------------------------- /App/PlayBooksWeb/static/external/css/bootstrap-toggle.min.css: -------------------------------------------------------------------------------- 1 | /*! ======================================================================== 2 | * Bootstrap Toggle: bootstrap-toggle.css v2.2.0 3 | * http://www.bootstraptoggle.com 4 | * ======================================================================== 5 | * Copyright 2014 Min Hur, The New York Times Company 6 | * Licensed under MIT 7 | * ======================================================================== */ 8 | .checkbox label .toggle,.checkbox-inline .toggle{margin-left:-20px;margin-right:5px} 9 | .toggle{position:relative;overflow:hidden} 10 | .toggle input[type=checkbox]{display:none} 11 | .toggle-group{position:absolute;width:200%;top:0;bottom:0;left:0;transition:left .35s;-webkit-transition:left .35s;-moz-user-select:none;-webkit-user-select:none} 12 | .toggle.off .toggle-group{left:-100%} 13 | .toggle-on{position:absolute;top:0;bottom:0;left:0;right:50%;margin:0;border:0;border-radius:0} 14 | .toggle-off{position:absolute;top:0;bottom:0;left:50%;right:0;margin:0;border:0;border-radius:0} 15 | .toggle-handle{position:relative;margin:0 auto;padding-top:0;padding-bottom:0;height:100%;width:0;border-width:0 1px} 16 | .toggle.btn{min-width:59px;min-height:34px} 17 | .toggle-on.btn{padding-right:24px} 18 | .toggle-off.btn{padding-left:24px} 19 | .toggle.btn-lg{min-width:79px;min-height:45px} 20 | .toggle-on.btn-lg{padding-right:31px} 21 | .toggle-off.btn-lg{padding-left:31px} 22 | .toggle-handle.btn-lg{width:40px} 23 | .toggle.btn-sm{min-width:50px;min-height:30px} 24 | .toggle-on.btn-sm{padding-right:20px} 25 | .toggle-off.btn-sm{padding-left:20px} 26 | .toggle.btn-xs{min-width:35px;min-height:22px} 27 | .toggle-on.btn-xs{padding-right:12px} 28 | .toggle-off.btn-xs{padding-left:12px} -------------------------------------------------------------------------------- /App/PlayBooksWeb/static/external/css/jquery-confirm.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * jquery-confirm v3.3.4 (http://craftpip.github.io/jquery-confirm/) 3 | * Author: boniface pereira 4 | * Website: www.craftpip.com 5 | * Contact: hey@craftpip.com 6 | * 7 | * Copyright 2013-2019 jquery-confirm 8 | * Licensed under MIT (https://github.com/craftpip/jquery-confirm/blob/master/LICENSE) 9 | */@-webkit-keyframes jconfirm-spin{from{-webkit-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}@keyframes jconfirm-spin{from{-webkit-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}body[class*=jconfirm-no-scroll-]{overflow:hidden!important}.jconfirm{position:fixed;top:0;left:0;right:0;bottom:0;z-index:99999999;font-family:inherit;overflow:hidden}.jconfirm .jconfirm-bg{position:fixed;top:0;left:0;right:0;bottom:0;-webkit-transition:opacity .4s;transition:opacity .4s}.jconfirm .jconfirm-bg.jconfirm-bg-h{opacity:0!important}.jconfirm .jconfirm-scrollpane{-webkit-perspective:500px;perspective:500px;-webkit-perspective-origin:center;perspective-origin:center;display:table;width:100%;height:100%}.jconfirm .jconfirm-row{display:table-row;width:100%}.jconfirm .jconfirm-cell{display:table-cell;vertical-align:middle}.jconfirm .jconfirm-holder{max-height:100%;padding:50px 0}.jconfirm .jconfirm-box-container{-webkit-transition:-webkit-transform;transition:-webkit-transform;transition:transform;transition:transform,-webkit-transform}.jconfirm .jconfirm-box-container.jconfirm-no-transition{-webkit-transition:none!important;transition:none!important}.jconfirm .jconfirm-box{background:white;border-radius:4px;position:relative;outline:0;padding:15px 15px 0;overflow:hidden;margin-left:auto;margin-right:auto}@-webkit-keyframes type-blue{1%,100%{border-color:#3498db}50%{border-color:#5faee3}}@keyframes type-blue{1%,100%{border-color:#3498db}50%{border-color:#5faee3}}@-webkit-keyframes type-green{1%,100%{border-color:#2ecc71}50%{border-color:#54d98c}}@keyframes type-green{1%,100%{border-color:#2ecc71}50%{border-color:#54d98c}}@-webkit-keyframes type-red{1%,100%{border-color:#e74c3c}50%{border-color:#ed7669}}@keyframes type-red{1%,100%{border-color:#e74c3c}50%{border-color:#ed7669}}@-webkit-keyframes type-orange{1%,100%{border-color:#f1c40f}50%{border-color:#f4d03f}}@keyframes type-orange{1%,100%{border-color:#f1c40f}50%{border-color:#f4d03f}}@-webkit-keyframes type-purple{1%,100%{border-color:#9b59b6}50%{border-color:#b07cc6}}@keyframes type-purple{1%,100%{border-color:#9b59b6}50%{border-color:#b07cc6}}@-webkit-keyframes type-dark{1%,100%{border-color:#34495e}50%{border-color:#46627f}}@keyframes type-dark{1%,100%{border-color:#34495e}50%{border-color:#46627f}}.jconfirm .jconfirm-box.jconfirm-type-animated{-webkit-animation-duration:2s;animation-duration:2s;-webkit-animation-iteration-count:infinite;animation-iteration-count:infinite}.jconfirm .jconfirm-box.jconfirm-type-blue{border-top:solid 7px #3498db;-webkit-animation-name:type-blue;animation-name:type-blue}.jconfirm .jconfirm-box.jconfirm-type-green{border-top:solid 7px #2ecc71;-webkit-animation-name:type-green;animation-name:type-green}.jconfirm .jconfirm-box.jconfirm-type-red{border-top:solid 7px #e74c3c;-webkit-animation-name:type-red;animation-name:type-red}.jconfirm .jconfirm-box.jconfirm-type-orange{border-top:solid 7px #f1c40f;-webkit-animation-name:type-orange;animation-name:type-orange}.jconfirm .jconfirm-box.jconfirm-type-purple{border-top:solid 7px #9b59b6;-webkit-animation-name:type-purple;animation-name:type-purple}.jconfirm .jconfirm-box.jconfirm-type-dark{border-top:solid 7px #34495e;-webkit-animation-name:type-dark;animation-name:type-dark}.jconfirm .jconfirm-box.loading{height:120px}.jconfirm .jconfirm-box.loading:before{content:'';position:absolute;left:0;background:white;right:0;top:0;bottom:0;border-radius:10px;z-index:1}.jconfirm .jconfirm-box.loading:after{opacity:.6;content:'';height:30px;width:30px;border:solid 3px transparent;position:absolute;left:50%;margin-left:-15px;border-radius:50%;-webkit-animation:jconfirm-spin 1s infinite linear;animation:jconfirm-spin 1s infinite linear;border-bottom-color:dodgerblue;top:50%;margin-top:-15px;z-index:2}.jconfirm .jconfirm-box div.jconfirm-closeIcon{height:20px;width:20px;position:absolute;top:10px;right:10px;cursor:pointer;opacity:.6;text-align:center;font-size:27px!important;line-height:14px!important;display:none;z-index:1}.jconfirm .jconfirm-box div.jconfirm-closeIcon:empty{display:none}.jconfirm .jconfirm-box div.jconfirm-closeIcon .fa{font-size:16px}.jconfirm .jconfirm-box div.jconfirm-closeIcon .glyphicon{font-size:16px}.jconfirm .jconfirm-box div.jconfirm-closeIcon .zmdi{font-size:16px}.jconfirm .jconfirm-box div.jconfirm-closeIcon:hover{opacity:1}.jconfirm .jconfirm-box div.jconfirm-title-c{display:block;font-size:22px;line-height:20px;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;cursor:default;padding-bottom:15px}.jconfirm .jconfirm-box div.jconfirm-title-c.jconfirm-hand{cursor:move}.jconfirm .jconfirm-box div.jconfirm-title-c .jconfirm-icon-c{font-size:inherit;display:inline-block;vertical-align:middle}.jconfirm .jconfirm-box div.jconfirm-title-c .jconfirm-icon-c i{vertical-align:middle}.jconfirm .jconfirm-box div.jconfirm-title-c .jconfirm-icon-c:empty{display:none}.jconfirm .jconfirm-box div.jconfirm-title-c .jconfirm-title{-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;font-size:inherit;font-family:inherit;display:inline-block;vertical-align:middle}.jconfirm .jconfirm-box div.jconfirm-title-c .jconfirm-title:empty{display:none}.jconfirm .jconfirm-box div.jconfirm-content-pane{margin-bottom:15px;height:auto;-webkit-transition:height .4s ease-in;transition:height .4s ease-in;display:inline-block;width:100%;position:relative;overflow-x:hidden;overflow-y:auto}.jconfirm .jconfirm-box div.jconfirm-content-pane.no-scroll{overflow-y:hidden}.jconfirm .jconfirm-box div.jconfirm-content-pane::-webkit-scrollbar{width:3px}.jconfirm .jconfirm-box div.jconfirm-content-pane::-webkit-scrollbar-track{background:rgba(0,0,0,0.1)}.jconfirm .jconfirm-box div.jconfirm-content-pane::-webkit-scrollbar-thumb{background:#666;border-radius:3px}.jconfirm .jconfirm-box div.jconfirm-content-pane .jconfirm-content{overflow:auto}.jconfirm .jconfirm-box div.jconfirm-content-pane .jconfirm-content img{max-width:100%;height:auto}.jconfirm .jconfirm-box div.jconfirm-content-pane .jconfirm-content:empty{display:none}.jconfirm .jconfirm-box .jconfirm-buttons{padding-bottom:11px}.jconfirm .jconfirm-box .jconfirm-buttons>button{margin-bottom:4px;margin-left:2px;margin-right:2px}.jconfirm .jconfirm-box .jconfirm-buttons button{display:inline-block;padding:6px 12px;font-size:14px;font-weight:400;line-height:1.42857143;text-align:center;white-space:nowrap;vertical-align:middle;-ms-touch-action:manipulation;touch-action:manipulation;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;border-radius:4px;min-height:1em;-webkit-transition:opacity .1s ease,background-color .1s ease,color .1s ease,background .1s ease,-webkit-box-shadow .1s ease;transition:opacity .1s ease,background-color .1s ease,color .1s ease,background .1s ease,-webkit-box-shadow .1s ease;transition:opacity .1s ease,background-color .1s ease,color .1s ease,box-shadow .1s ease,background .1s ease;transition:opacity .1s ease,background-color .1s ease,color .1s ease,box-shadow .1s ease,background .1s ease,-webkit-box-shadow .1s ease;-webkit-tap-highlight-color:transparent;border:0;background-image:none}.jconfirm .jconfirm-box .jconfirm-buttons button.btn-blue{background-color:#3498db;color:#FFF;text-shadow:none;-webkit-transition:background .2s;transition:background .2s}.jconfirm .jconfirm-box .jconfirm-buttons button.btn-blue:hover{background-color:#2980b9;color:#FFF}.jconfirm .jconfirm-box .jconfirm-buttons button.btn-green{background-color:#2ecc71;color:#FFF;text-shadow:none;-webkit-transition:background .2s;transition:background .2s}.jconfirm .jconfirm-box .jconfirm-buttons button.btn-green:hover{background-color:#27ae60;color:#FFF}.jconfirm .jconfirm-box .jconfirm-buttons button.btn-red{background-color:#e74c3c;color:#FFF;text-shadow:none;-webkit-transition:background .2s;transition:background .2s}.jconfirm .jconfirm-box .jconfirm-buttons button.btn-red:hover{background-color:#c0392b;color:#FFF}.jconfirm .jconfirm-box .jconfirm-buttons button.btn-orange{background-color:#f1c40f;color:#FFF;text-shadow:none;-webkit-transition:background .2s;transition:background .2s}.jconfirm .jconfirm-box .jconfirm-buttons button.btn-orange:hover{background-color:#f39c12;color:#FFF}.jconfirm .jconfirm-box .jconfirm-buttons button.btn-default{background-color:#ecf0f1;color:#000;text-shadow:none;-webkit-transition:background .2s;transition:background .2s}.jconfirm .jconfirm-box .jconfirm-buttons button.btn-default:hover{background-color:#bdc3c7;color:#000}.jconfirm .jconfirm-box .jconfirm-buttons button.btn-purple{background-color:#9b59b6;color:#FFF;text-shadow:none;-webkit-transition:background .2s;transition:background .2s}.jconfirm .jconfirm-box .jconfirm-buttons button.btn-purple:hover{background-color:#8e44ad;color:#FFF}.jconfirm .jconfirm-box .jconfirm-buttons button.btn-dark{background-color:#34495e;color:#FFF;text-shadow:none;-webkit-transition:background .2s;transition:background .2s}.jconfirm .jconfirm-box .jconfirm-buttons button.btn-dark:hover{background-color:#2c3e50;color:#FFF}.jconfirm .jconfirm-box.jconfirm-type-red .jconfirm-title-c .jconfirm-icon-c{color:#e74c3c!important}.jconfirm .jconfirm-box.jconfirm-type-blue .jconfirm-title-c .jconfirm-icon-c{color:#3498db!important}.jconfirm .jconfirm-box.jconfirm-type-green .jconfirm-title-c .jconfirm-icon-c{color:#2ecc71!important}.jconfirm .jconfirm-box.jconfirm-type-purple .jconfirm-title-c .jconfirm-icon-c{color:#9b59b6!important}.jconfirm .jconfirm-box.jconfirm-type-orange .jconfirm-title-c .jconfirm-icon-c{color:#f1c40f!important}.jconfirm .jconfirm-box.jconfirm-type-dark .jconfirm-title-c .jconfirm-icon-c{color:#34495e!important}.jconfirm .jconfirm-clear{clear:both}.jconfirm.jconfirm-rtl{direction:rtl}.jconfirm.jconfirm-rtl div.jconfirm-closeIcon{left:5px;right:auto}.jconfirm.jconfirm-white .jconfirm-bg,.jconfirm.jconfirm-light .jconfirm-bg{background-color:#444;opacity:.2}.jconfirm.jconfirm-white .jconfirm-box,.jconfirm.jconfirm-light .jconfirm-box{-webkit-box-shadow:0 2px 6px rgba(0,0,0,0.2);box-shadow:0 2px 6px rgba(0,0,0,0.2);border-radius:5px}.jconfirm.jconfirm-white .jconfirm-box .jconfirm-title-c .jconfirm-icon-c,.jconfirm.jconfirm-light .jconfirm-box .jconfirm-title-c .jconfirm-icon-c{margin-right:8px;margin-left:0}.jconfirm.jconfirm-white .jconfirm-box .jconfirm-buttons,.jconfirm.jconfirm-light .jconfirm-box .jconfirm-buttons{float:right}.jconfirm.jconfirm-white .jconfirm-box .jconfirm-buttons button,.jconfirm.jconfirm-light .jconfirm-box .jconfirm-buttons button{text-transform:uppercase;font-size:14px;font-weight:bold;text-shadow:none}.jconfirm.jconfirm-white .jconfirm-box .jconfirm-buttons button.btn-default,.jconfirm.jconfirm-light .jconfirm-box .jconfirm-buttons button.btn-default{-webkit-box-shadow:none;box-shadow:none;color:#333}.jconfirm.jconfirm-white .jconfirm-box .jconfirm-buttons button.btn-default:hover,.jconfirm.jconfirm-light .jconfirm-box .jconfirm-buttons button.btn-default:hover{background:#ddd}.jconfirm.jconfirm-white.jconfirm-rtl .jconfirm-title-c .jconfirm-icon-c,.jconfirm.jconfirm-light.jconfirm-rtl .jconfirm-title-c .jconfirm-icon-c{margin-left:8px;margin-right:0}.jconfirm.jconfirm-black .jconfirm-bg,.jconfirm.jconfirm-dark .jconfirm-bg{background-color:darkslategray;opacity:.4}.jconfirm.jconfirm-black .jconfirm-box,.jconfirm.jconfirm-dark .jconfirm-box{-webkit-box-shadow:0 2px 6px rgba(0,0,0,0.2);box-shadow:0 2px 6px rgba(0,0,0,0.2);background:#444;border-radius:5px;color:white}.jconfirm.jconfirm-black .jconfirm-box .jconfirm-title-c .jconfirm-icon-c,.jconfirm.jconfirm-dark .jconfirm-box .jconfirm-title-c .jconfirm-icon-c{margin-right:8px;margin-left:0}.jconfirm.jconfirm-black .jconfirm-box .jconfirm-buttons,.jconfirm.jconfirm-dark .jconfirm-box .jconfirm-buttons{float:right}.jconfirm.jconfirm-black .jconfirm-box .jconfirm-buttons button,.jconfirm.jconfirm-dark .jconfirm-box .jconfirm-buttons button{border:0;background-image:none;text-transform:uppercase;font-size:14px;font-weight:bold;text-shadow:none;-webkit-transition:background .1s;transition:background .1s;color:white}.jconfirm.jconfirm-black .jconfirm-box .jconfirm-buttons button.btn-default,.jconfirm.jconfirm-dark .jconfirm-box .jconfirm-buttons button.btn-default{-webkit-box-shadow:none;box-shadow:none;color:#fff;background:0}.jconfirm.jconfirm-black .jconfirm-box .jconfirm-buttons button.btn-default:hover,.jconfirm.jconfirm-dark .jconfirm-box .jconfirm-buttons button.btn-default:hover{background:#666}.jconfirm.jconfirm-black.jconfirm-rtl .jconfirm-title-c .jconfirm-icon-c,.jconfirm.jconfirm-dark.jconfirm-rtl .jconfirm-title-c .jconfirm-icon-c{margin-left:8px;margin-right:0}.jconfirm .jconfirm-box.hilight.jconfirm-hilight-shake{-webkit-animation:shake .82s cubic-bezier(0.36,0.07,0.19,0.97) both;animation:shake .82s cubic-bezier(0.36,0.07,0.19,0.97) both;-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}.jconfirm .jconfirm-box.hilight.jconfirm-hilight-glow{-webkit-animation:glow .82s cubic-bezier(0.36,0.07,0.19,0.97) both;animation:glow .82s cubic-bezier(0.36,0.07,0.19,0.97) both;-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}@-webkit-keyframes shake{10%,90%{-webkit-transform:translate3d(-2px,0,0);transform:translate3d(-2px,0,0)}20%,80%{-webkit-transform:translate3d(4px,0,0);transform:translate3d(4px,0,0)}30%,50%,70%{-webkit-transform:translate3d(-8px,0,0);transform:translate3d(-8px,0,0)}40%,60%{-webkit-transform:translate3d(8px,0,0);transform:translate3d(8px,0,0)}}@keyframes shake{10%,90%{-webkit-transform:translate3d(-2px,0,0);transform:translate3d(-2px,0,0)}20%,80%{-webkit-transform:translate3d(4px,0,0);transform:translate3d(4px,0,0)}30%,50%,70%{-webkit-transform:translate3d(-8px,0,0);transform:translate3d(-8px,0,0)}40%,60%{-webkit-transform:translate3d(8px,0,0);transform:translate3d(8px,0,0)}}@-webkit-keyframes glow{0%,100%{-webkit-box-shadow:0 0 0 red;box-shadow:0 0 0 red}50%{-webkit-box-shadow:0 0 30px red;box-shadow:0 0 30px red}}@keyframes glow{0%,100%{-webkit-box-shadow:0 0 0 red;box-shadow:0 0 0 red}50%{-webkit-box-shadow:0 0 30px red;box-shadow:0 0 30px red}}.jconfirm{-webkit-perspective:400px;perspective:400px}.jconfirm .jconfirm-box{opacity:1;-webkit-transition-property:all;transition-property:all}.jconfirm .jconfirm-box.jconfirm-animation-top,.jconfirm .jconfirm-box.jconfirm-animation-left,.jconfirm .jconfirm-box.jconfirm-animation-right,.jconfirm .jconfirm-box.jconfirm-animation-bottom,.jconfirm .jconfirm-box.jconfirm-animation-opacity,.jconfirm .jconfirm-box.jconfirm-animation-zoom,.jconfirm .jconfirm-box.jconfirm-animation-scale,.jconfirm .jconfirm-box.jconfirm-animation-none,.jconfirm .jconfirm-box.jconfirm-animation-rotate,.jconfirm .jconfirm-box.jconfirm-animation-rotatex,.jconfirm .jconfirm-box.jconfirm-animation-rotatey,.jconfirm .jconfirm-box.jconfirm-animation-scaley,.jconfirm .jconfirm-box.jconfirm-animation-scalex{opacity:0}.jconfirm .jconfirm-box.jconfirm-animation-rotate{-webkit-transform:rotate(90deg);transform:rotate(90deg)}.jconfirm .jconfirm-box.jconfirm-animation-rotatex{-webkit-transform:rotateX(90deg);transform:rotateX(90deg);-webkit-transform-origin:center;transform-origin:center}.jconfirm .jconfirm-box.jconfirm-animation-rotatexr{-webkit-transform:rotateX(-90deg);transform:rotateX(-90deg);-webkit-transform-origin:center;transform-origin:center}.jconfirm .jconfirm-box.jconfirm-animation-rotatey{-webkit-transform:rotatey(90deg);transform:rotatey(90deg);-webkit-transform-origin:center;transform-origin:center}.jconfirm .jconfirm-box.jconfirm-animation-rotateyr{-webkit-transform:rotatey(-90deg);transform:rotatey(-90deg);-webkit-transform-origin:center;transform-origin:center}.jconfirm .jconfirm-box.jconfirm-animation-scaley{-webkit-transform:scaley(1.5);transform:scaley(1.5);-webkit-transform-origin:center;transform-origin:center}.jconfirm .jconfirm-box.jconfirm-animation-scalex{-webkit-transform:scalex(1.5);transform:scalex(1.5);-webkit-transform-origin:center;transform-origin:center}.jconfirm .jconfirm-box.jconfirm-animation-top{-webkit-transform:translate(0px,-100px);transform:translate(0px,-100px)}.jconfirm .jconfirm-box.jconfirm-animation-left{-webkit-transform:translate(-100px,0px);transform:translate(-100px,0px)}.jconfirm .jconfirm-box.jconfirm-animation-right{-webkit-transform:translate(100px,0px);transform:translate(100px,0px)}.jconfirm .jconfirm-box.jconfirm-animation-bottom{-webkit-transform:translate(0px,100px);transform:translate(0px,100px)}.jconfirm .jconfirm-box.jconfirm-animation-zoom{-webkit-transform:scale(1.2);transform:scale(1.2)}.jconfirm .jconfirm-box.jconfirm-animation-scale{-webkit-transform:scale(0.5);transform:scale(0.5)}.jconfirm .jconfirm-box.jconfirm-animation-none{visibility:hidden}.jconfirm.jconfirm-supervan .jconfirm-bg{background-color:rgba(54,70,93,0.95)}.jconfirm.jconfirm-supervan .jconfirm-box{background-color:transparent}.jconfirm.jconfirm-supervan .jconfirm-box.jconfirm-type-blue{border:0}.jconfirm.jconfirm-supervan .jconfirm-box.jconfirm-type-green{border:0}.jconfirm.jconfirm-supervan .jconfirm-box.jconfirm-type-red{border:0}.jconfirm.jconfirm-supervan .jconfirm-box.jconfirm-type-orange{border:0}.jconfirm.jconfirm-supervan .jconfirm-box.jconfirm-type-purple{border:0}.jconfirm.jconfirm-supervan .jconfirm-box.jconfirm-type-dark{border:0}.jconfirm.jconfirm-supervan .jconfirm-box div.jconfirm-closeIcon{color:white}.jconfirm.jconfirm-supervan .jconfirm-box div.jconfirm-title-c{text-align:center;color:white;font-size:28px;font-weight:normal}.jconfirm.jconfirm-supervan .jconfirm-box div.jconfirm-title-c>*{padding-bottom:25px}.jconfirm.jconfirm-supervan .jconfirm-box div.jconfirm-title-c .jconfirm-icon-c{margin-right:8px;margin-left:0}.jconfirm.jconfirm-supervan .jconfirm-box div.jconfirm-content-pane{margin-bottom:25px}.jconfirm.jconfirm-supervan .jconfirm-box div.jconfirm-content{text-align:center;color:white}.jconfirm.jconfirm-supervan .jconfirm-box .jconfirm-buttons{text-align:center}.jconfirm.jconfirm-supervan .jconfirm-box .jconfirm-buttons button{font-size:16px;border-radius:2px;background:#303f53;text-shadow:none;border:0;color:white;padding:10px;min-width:100px}.jconfirm.jconfirm-supervan.jconfirm-rtl .jconfirm-box div.jconfirm-title-c .jconfirm-icon-c{margin-left:8px;margin-right:0}.jconfirm.jconfirm-material .jconfirm-bg{background-color:rgba(0,0,0,0.67)}.jconfirm.jconfirm-material .jconfirm-box{background-color:white;-webkit-box-shadow:0 7px 8px -4px rgba(0,0,0,0.2),0 13px 19px 2px rgba(0,0,0,0.14),0 5px 24px 4px rgba(0,0,0,0.12);box-shadow:0 7px 8px -4px rgba(0,0,0,0.2),0 13px 19px 2px rgba(0,0,0,0.14),0 5px 24px 4px rgba(0,0,0,0.12);padding:30px 25px 10px 25px}.jconfirm.jconfirm-material .jconfirm-box .jconfirm-title-c .jconfirm-icon-c{margin-right:8px;margin-left:0}.jconfirm.jconfirm-material .jconfirm-box div.jconfirm-closeIcon{color:rgba(0,0,0,0.87)}.jconfirm.jconfirm-material .jconfirm-box div.jconfirm-title-c{color:rgba(0,0,0,0.87);font-size:22px;font-weight:bold}.jconfirm.jconfirm-material .jconfirm-box div.jconfirm-content{color:rgba(0,0,0,0.87)}.jconfirm.jconfirm-material .jconfirm-box .jconfirm-buttons{text-align:right}.jconfirm.jconfirm-material .jconfirm-box .jconfirm-buttons button{text-transform:uppercase;font-weight:500}.jconfirm.jconfirm-material.jconfirm-rtl .jconfirm-title-c .jconfirm-icon-c{margin-left:8px;margin-right:0}.jconfirm.jconfirm-bootstrap .jconfirm-bg{background-color:rgba(0,0,0,0.21)}.jconfirm.jconfirm-bootstrap .jconfirm-box{background-color:white;-webkit-box-shadow:0 3px 8px 0 rgba(0,0,0,0.2);box-shadow:0 3px 8px 0 rgba(0,0,0,0.2);border:solid 1px rgba(0,0,0,0.4);padding:15px 0 0}.jconfirm.jconfirm-bootstrap .jconfirm-box .jconfirm-title-c .jconfirm-icon-c{margin-right:8px;margin-left:0}.jconfirm.jconfirm-bootstrap .jconfirm-box div.jconfirm-closeIcon{color:rgba(0,0,0,0.87)}.jconfirm.jconfirm-bootstrap .jconfirm-box div.jconfirm-title-c{color:rgba(0,0,0,0.87);font-size:22px;font-weight:bold;padding-left:15px;padding-right:15px}.jconfirm.jconfirm-bootstrap .jconfirm-box div.jconfirm-content{color:rgba(0,0,0,0.87);padding:0 15px}.jconfirm.jconfirm-bootstrap .jconfirm-box .jconfirm-buttons{text-align:right;padding:10px;margin:-5px 0 0;border-top:solid 1px #ddd;overflow:hidden;border-radius:0 0 4px 4px}.jconfirm.jconfirm-bootstrap .jconfirm-box .jconfirm-buttons button{font-weight:500}.jconfirm.jconfirm-bootstrap.jconfirm-rtl .jconfirm-title-c .jconfirm-icon-c{margin-left:8px;margin-right:0}.jconfirm.jconfirm-modern .jconfirm-bg{background-color:slategray;opacity:.6}.jconfirm.jconfirm-modern .jconfirm-box{background-color:white;-webkit-box-shadow:0 7px 8px -4px rgba(0,0,0,0.2),0 13px 19px 2px rgba(0,0,0,0.14),0 5px 24px 4px rgba(0,0,0,0.12);box-shadow:0 7px 8px -4px rgba(0,0,0,0.2),0 13px 19px 2px rgba(0,0,0,0.14),0 5px 24px 4px rgba(0,0,0,0.12);padding:30px 30px 15px}.jconfirm.jconfirm-modern .jconfirm-box div.jconfirm-closeIcon{color:rgba(0,0,0,0.87);top:15px;right:15px}.jconfirm.jconfirm-modern .jconfirm-box div.jconfirm-title-c{color:rgba(0,0,0,0.87);font-size:24px;font-weight:bold;text-align:center;margin-bottom:10px}.jconfirm.jconfirm-modern .jconfirm-box div.jconfirm-title-c .jconfirm-icon-c{-webkit-transition:-webkit-transform .5s;transition:-webkit-transform .5s;transition:transform .5s;transition:transform .5s,-webkit-transform .5s;-webkit-transform:scale(0);transform:scale(0);display:block;margin-right:0;margin-left:0;margin-bottom:10px;font-size:69px;color:#aaa}.jconfirm.jconfirm-modern .jconfirm-box div.jconfirm-content{text-align:center;font-size:15px;color:#777;margin-bottom:25px}.jconfirm.jconfirm-modern .jconfirm-box .jconfirm-buttons{text-align:center}.jconfirm.jconfirm-modern .jconfirm-box .jconfirm-buttons button{font-weight:bold;text-transform:uppercase;-webkit-transition:background .1s;transition:background .1s;padding:10px 20px}.jconfirm.jconfirm-modern .jconfirm-box .jconfirm-buttons button+button{margin-left:4px}.jconfirm.jconfirm-modern.jconfirm-open .jconfirm-box .jconfirm-title-c .jconfirm-icon-c{-webkit-transform:scale(1);transform:scale(1)} -------------------------------------------------------------------------------- /App/PlayBooksWeb/static/external/js/bootstrap-toggle.min.js: -------------------------------------------------------------------------------- 1 | /*! ======================================================================== 2 | * Bootstrap Toggle: bootstrap-toggle.js v2.2.0 3 | * http://www.bootstraptoggle.com 4 | * ======================================================================== 5 | * Copyright 2014 Min Hur, The New York Times Company 6 | * Licensed under MIT 7 | * ======================================================================== */ 8 | +function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.toggle"),f="object"==typeof b&&b;e||d.data("bs.toggle",e=new c(this,f)),"string"==typeof b&&e[b]&&e[b]()})}var c=function(b,c){this.$element=a(b),this.options=a.extend({},this.defaults(),c),this.render()};c.VERSION="2.2.0",c.DEFAULTS={on:"On",off:"Off",onstyle:"primary",offstyle:"default",size:"normal",style:"",width:null,height:null},c.prototype.defaults=function(){return{on:this.$element.attr("data-on")||c.DEFAULTS.on,off:this.$element.attr("data-off")||c.DEFAULTS.off,onstyle:this.$element.attr("data-onstyle")||c.DEFAULTS.onstyle,offstyle:this.$element.attr("data-offstyle")||c.DEFAULTS.offstyle,size:this.$element.attr("data-size")||c.DEFAULTS.size,style:this.$element.attr("data-style")||c.DEFAULTS.style,width:this.$element.attr("data-width")||c.DEFAULTS.width,height:this.$element.attr("data-height")||c.DEFAULTS.height}},c.prototype.render=function(){this._onstyle="btn-"+this.options.onstyle,this._offstyle="btn-"+this.options.offstyle;var b="large"===this.options.size?"btn-lg":"small"===this.options.size?"btn-sm":"mini"===this.options.size?"btn-xs":"",c=a('