diff --git a/resume_manager_core/fixtures/resume_manager/resume_sections.json b/resume_manager_core/fixtures/resume_manager/resume_sections.json new file mode 100644 index 0000000000000000000000000000000000000000..24d854343d5e80b3c89524a8df9145dea5b968a3 --- /dev/null +++ b/resume_manager_core/fixtures/resume_manager/resume_sections.json @@ -0,0 +1,172 @@ +[ +{ + "model": "resume_manager_core.resumesection", + "pk": 1, + "fields": { + "name": "Page", + "css_classes": "resume-page full-size", + "date_created": "2023-01-01T08:00:00.000Z", + "date_modified": "2023-01-01T08:00:00.000Z" + } +}, +{ + "model": "resume_manager_core.resumesection", + "pk": 2, + "fields": { + "name": "Row: Full", + "css_classes": "resume-section-row full-size", + "date_created": "2023-01-01T08:00:00.000Z", + "date_modified": "2023-01-01T08:00:00.000Z" + } +}, +{ + "model": "resume_manager_core.resumesection", + "pk": 3, + "fields": { + "name": "Row: Half", + "css_classes": "resume-section-row full-size", + "date_created": "2023-01-01T08:00:00.000Z", + "date_modified": "2023-01-01T08:00:00.000Z" + } +}, +{ + "model": "resume_manager_core.resumesection", + "pk": 4, + "fields": { + "name": "Row: One-Third", + "css_classes": "resume-section-row one-third-size", + "date_created": "2023-01-01T08:00:00.000Z", + "date_modified": "2023-01-01T08:00:00.000Z" + } +}, +{ + "model": "resume_manager_core.resumesection", + "pk": 5, + "fields": { + "name": "Row: Two-Thirds", + "css_classes": "resume-section-row two-thirds-size", + "date_created": "2023-01-01T08:00:00.000Z", + "date_modified": "2023-01-01T08:00:00.000Z" + } +}, +{ + "model": "resume_manager_core.resumesection", + "pk": 6, + "fields": { + "name": "Row: One-Fourth", + "css_classes": "resume-section-row one-fourth-size", + "date_created": "2023-01-01T08:00:00.000Z", + "date_modified": "2023-01-01T08:00:00.000Z" + } +}, +{ + "model": "resume_manager_core.resumesection", + "pk": 7, + "fields": { + "name": "Row: Three-Fourths", + "css_classes": "resume-section-row three-fourths-size", + "date_created": "2023-01-01T08:00:00.000Z", + "date_modified": "2023-01-01T08:00:00.000Z" + } +}, +{ + "model": "resume_manager_core.resumesection", + "pk": 8, + "fields": { + "name": "Column: Full", + "css_classes": "resume-section-column full-size", + "date_created": "2023-01-01T08:00:00.000Z", + "date_modified": "2023-01-01T08:00:00.000Z" + } +}, +{ + "model": "resume_manager_core.resumesection", + "pk": 9, + "fields": { + "name": "Column: Half", + "css_classes": "resume-section-column half-size", + "date_created": "2023-01-01T08:00:00.000Z", + "date_modified": "2023-01-01T08:00:00.000Z" + } +}, +{ + "model": "resume_manager_core.resumesection", + "pk": 10, + "fields": { + "name": "Column: One-Third", + "css_classes": "resume-section-column one-third-size", + "date_created": "2023-01-01T08:00:00.000Z", + "date_modified": "2023-01-01T08:00:00.000Z" + } +}, +{ + "model": "resume_manager_core.resumesection", + "pk": 11, + "fields": { + "name": "Column: Two-Thirds", + "css_classes": "resume-section-column two-thirds-size", + "date_created": "2023-01-01T08:00:00.000Z", + "date_modified": "2023-01-01T08:00:00.000Z" + } +}, +{ + "model": "resume_manager_core.resumesection", + "pk": 12, + "fields": { + "name": "Column: One-Fourth", + "css_classes": "resume-section-column one-fourth-size", + "date_created": "2023-01-01T08:00:00.000Z", + "date_modified": "2023-01-01T08:00:00.000Z" + } +}, +{ + "model": "resume_manager_core.resumesection", + "pk": 13, + "fields": { + "name": "Column: Three-Fourths", + "css_classes": "resume-section-column three-fourths-size", + "date_created": "2023-01-01T08:00:00.000Z", + "date_modified": "2023-01-01T08:00:00.000Z" + } +}, +{ + "model": "resume_manager_core.resumesection", + "pk": 14, + "fields": { + "name": "Text: Left-Align", + "css_classes": "resume-section-text left-align", + "date_created": "2023-01-01T08:00:00.000Z", + "date_modified": "2023-01-01T08:00:00.000Z" + } +}, +{ + "model": "resume_manager_core.resumesection", + "pk": 15, + "fields": { + "name": "Text: Center-Align", + "css_classes": "resume-section-text center-align", + "date_created": "2023-01-01T08:00:00.000Z", + "date_modified": "2023-01-01T08:00:00.000Z" + } +}, +{ + "model": "resume_manager_core.resumesection", + "pk": 16, + "fields": { + "name": "Text: Right-Align", + "css_classes": "resume-section-text right-align", + "date_created": "2023-01-01T08:00:00.000Z", + "date_modified": "2023-01-01T08:00:00.000Z" + } +}, +{ + "model": "resume_manager_core.resumesection", + "pk": 17, + "fields": { + "name": "Style: Full Color", + "css_classes": "resume-section-style full-color", + "date_created": "2023-01-01T08:00:00.000Z", + "date_modified": "2023-01-01T08:00:00.000Z" + } +} +] diff --git a/resume_manager_core/fixtures/resume_manager/resume_themes.json b/resume_manager_core/fixtures/resume_manager/resume_themes.json new file mode 100644 index 0000000000000000000000000000000000000000..c6b82f8313f661ee381bddd1e8b1e4281163fd53 --- /dev/null +++ b/resume_manager_core/fixtures/resume_manager/resume_themes.json @@ -0,0 +1,50 @@ +[ +{ + "model": "resume_manager_core.resumetheme", + "pk": 1, + "fields": { + "name": "Blocky Purple", + "css_classes": "theme-blocky purple", + "has_header_image": false, + "has_footer_image": false, + "date_created": "2023-01-01T08:00:00.000Z", + "date_modified": "2023-01-01T08:00:00.000Z" + } +}, +{ + "model": "resume_manager_core.resumetheme", + "pk": 2, + "fields": { + "name": "Blocky Red", + "css_classes": "theme-blocky red", + "has_header_image": false, + "has_footer_image": false, + "date_created": "2023-01-01T08:00:00.000Z", + "date_modified": "2023-01-01T08:00:00.000Z" + } +}, +{ + "model": "resume_manager_core.resumetheme", + "pk": 3, + "fields": { + "name": "Blocky Blue", + "css_classes": "theme-blocky blue", + "has_header_image": false, + "has_footer_image": false, + "date_created": "2023-01-01T08:00:00.000Z", + "date_modified": "2023-01-01T08:00:00.000Z" + } +}, +{ + "model": "resume_manager_core.resumetheme", + "pk": 4, + "fields": { + "name": "Blocky Green", + "css_classes": "theme-blocky green", + "has_header_image": false, + "has_footer_image": false, + "date_created": "2023-01-01T08:00:00.000Z", + "date_modified": "2023-01-01T08:00:00.000Z" + } +} +] diff --git a/resume_manager_core/management/__init__.py b/resume_manager_core/management/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/resume_manager_core/management/commands/__init__.py b/resume_manager_core/management/commands/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/resume_manager_core/management/commands/resume_manager_core__loadfixtures.py b/resume_manager_core/management/commands/resume_manager_core__loadfixtures.py new file mode 100644 index 0000000000000000000000000000000000000000..089e4e707363ad6ae82724b11f866d4556ba9535 --- /dev/null +++ b/resume_manager_core/management/commands/resume_manager_core__loadfixtures.py @@ -0,0 +1,179 @@ +""" +Command to load fixtures for Resume Manager Core models. +""" + +# Third-Party Imports. +from django.contrib.auth import get_user_model +from django.core.management import call_command +from django.core.management.base import BaseCommand + +# Internal Imports. +from apps.Resume_Manager.resume_manager_core.models import ( + Resume, + ResumeTheme, + ResumeSection, + ResumeToSectionIntermediary, + DataSectionHeader, + DataItem, +) +from workspace_core.settings.reusable_settings import ConsoleColors + + +class Command(BaseCommand): + help = 'Load fixtures into database models for Resume Manager Core app.' + + def handle(self, *args, **kwargs): + """ + The logic of the command. + """ + print('{0}RESUME_MANAGER_CORE{1}: {2}Load Fixtures{3} command has been called.'.format( + ConsoleColors.purple, + ConsoleColors.reset, + ConsoleColors.blue, + ConsoleColors.reset, + )) + + # Run fixture imports. + self.populate_resume_sections() + self.populate_resume_themes() + self.populate_resumes() + self.populate_resume_section_intermediaries() + self.populate_data_section_headers() + self.populate_data_section_items() + + print('{0}RESUME_MANAGER_CORE{1}: {2}Fixture Loading{3} complete.\n'.format( + ConsoleColors.purple, + ConsoleColors.reset, + ConsoleColors.blue, + ConsoleColors.reset, + )) + + def populate_resume_sections(self): + """ + Loads fixtures for ResumeSection models. + """ + call_command('loaddata', 'resume_manager/resume_sections.json') + + def populate_resume_themes(self): + """ + Loads fixtures for ResumeTheme models. + """ + call_command('loaddata', 'resume_manager/resume_themes.json') + + def populate_resumes(self): + """ + Loads fixtures for Resume models. + """ + # No fixtures yet. + pass + + def populate_resume_section_intermediaries(self): + """ + Loads fixtures for ResumeToSectionIntermediary models. + """ + + # Get "example_user" to associate all example resumes with. + example_user = get_user_model().objects.get(username='example_user') + + # Create an example resume for each theme. + resume_themes = ResumeTheme.objects.filter(name__startswith='Blocky ') + for resume_theme in resume_themes: + + # Create new example resume for theme. + current_resume = Resume.objects.create( + user=example_user, + theme=resume_theme, + name='Example {0} Resume'.format(resume_theme.name), + ) + + # Get resume section models. + section_format_page = ResumeSection.objects.get(name='Page') + section_format_row_full = ResumeSection.objects.get(name='Row: Full') + section_format_column_full = ResumeSection.objects.get(name='Column: Full') + section_format_text_left = ResumeSection.objects.get(name='Text: Left-Align') + section_format_text_center = ResumeSection.objects.get(name='Text: Center-Align') + section_format_text_right = ResumeSection.objects.get(name='Text: Right-Align') + section_format_style_full = ResumeSection.objects.get(name='Style: Full Color') + + # For each resume, create 4 pages to showcase different possible formats. + page_1 = ResumeToSectionIntermediary.objects.create( + resume=current_resume, + section_format=section_format_page, + ) + page_2 = ResumeToSectionIntermediary.objects.create( + resume=current_resume, + section_format=section_format_page, + ) + page_3 = ResumeToSectionIntermediary.objects.create( + resume=current_resume, + section_format=section_format_page, + ) + page_4 = ResumeToSectionIntermediary.objects.create( + resume=current_resume, + section_format=section_format_page, + ) + + # Create page 1 main subsections. + # Page one is in standard <header, middle, footer> format. + page_1_header = ResumeToSectionIntermediary.objects.create( + resume=current_resume, + parent_section=page_1, + section_format=section_format_row_full, + ) + page_1_header_background = ResumeToSectionIntermediary.objects.create( + resume=current_resume, + parent_section=page_1_header, + section_format=section_format_style_full, + ) + page_1_header_left = ResumeToSectionIntermediary.objects.create( + resume=current_resume, + parent_section=page_1_header_background, + section_format=section_format_row_full, + ) + page_1_header_name = ResumeToSectionIntermediary.objects.create( + resume=current_resume, + parent_section=page_1_header_left, + section_format=section_format_text_left, + ) + page_1_header_right = ResumeToSectionIntermediary.objects.create( + resume=current_resume, + parent_section=page_1_header_background, + section_format=section_format_column_full, + ) + page_1_header_profession = ResumeToSectionIntermediary.objects.create( + resume=current_resume, + parent_section=page_1_header_right, + section_format=section_format_text_center, + ) + page_1_middle = ResumeToSectionIntermediary.objects.create( + resume=current_resume, + parent_section=page_1, + section_format=section_format_column_full, + ) + page_1_footer = ResumeToSectionIntermediary.objects.create( + resume=current_resume, + parent_section=page_1, + section_format=section_format_row_full, + ) + page_1_footer_background = ResumeToSectionIntermediary.objects.create( + resume=current_resume, + parent_section=page_1_footer, + section_format=section_format_style_full, + ) + + # Create page 2 main subsections. + # Mimics a full resume in single-page format. + + def populate_data_section_headers(self): + """ + Loads fixtures for DataSectionHeader models. + """ + # No fixtures yet. + pass + + def populate_data_section_items(self): + """ + Loads fixtures for DataSectionItem models. + """ + # No fixtures yet. + pass diff --git a/resume_manager_core/management/commands/resume_manager_core__seed.py b/resume_manager_core/management/commands/resume_manager_core__seed.py new file mode 100644 index 0000000000000000000000000000000000000000..5685e951927ec5785d28ef832913d07e54d377e8 --- /dev/null +++ b/resume_manager_core/management/commands/resume_manager_core__seed.py @@ -0,0 +1,118 @@ +""" +Command to create seeds for ResumeManager app models. +""" + +# Third-Party Imports. +from django.core.management.base import BaseCommand + +# Internal Imports. +from . import resume_manager_core__loadfixtures +from workspace_core.settings.reusable_settings import ConsoleColors + + +# Initialize fixture class. +app_fixtures = resume_manager_core__loadfixtures.Command() + + +class Command(BaseCommand): + help = 'Seed database models with randomized data for ResumeManager app.' + + def add_arguments(self, parser): + """ + Parser for command. + """ + # Optional arguments. + parser.add_argument( + 'model_count', + type=int, + nargs='?', + default=100, + help='Number of randomized models to create. Defaults to 100. Cannot exceed 10,000.', + ) + + def handle(self, *args, **kwargs): + """ + The logic of the command. + """ + # Limit range of model_count variable. + model_count = kwargs['model_count'] + if model_count < 1: + model_count = 100 + elif model_count > 10000: + model_count = 100 + + print('{0}RESUME_MANAGER{1}: {2}Seed{3} command has been called.'.format( + ConsoleColors.purple, + ConsoleColors.reset, + ConsoleColors.blue, + ConsoleColors.reset, + )) + + # Run fixture imports. + self.create_resume_sections(model_count) + self.create_resume_themes(model_count) + self.create_resumes(model_count) + self.create_resume_section_intermediaries(model_count) + self.create_data_section_headers(model_count) + self.create_data_section_items(model_count) + + print('{0}RESUME_MANAGER{1}: {2}Seeding{3} complete.\n'.format( + ConsoleColors.purple, + ConsoleColors.reset, + ConsoleColors.blue, + ConsoleColors.reset, + )) + + def create_resume_sections(self, model_count): + """ + Create seeds for ResumeSection models. + """ + # Load preset fixtures. + app_fixtures.populate_resume_sections() + + # Seed logic goes here. + + def create_resume_themes(self, model_count): + """ + Create seeds for ResumeTheme models. + """ + # Load preset fixtures. + app_fixtures.populate_resume_themes() + + # Seed logic goes here. + + def create_resumes(self, model_count): + """ + Create seeds for Resume models. + """ + # Load preset fixtures. + app_fixtures.populate_resumes() + + # Seed logic goes here. + + def create_resume_section_intermediaries(self, model_count): + """ + Create seeds for ResumeToSectionIntermediary models. + """ + # Load preset fixtures. + app_fixtures.populate_resume_section_intermediaries() + + # Seed logic goes here. + + def create_data_section_headers(self, model_count): + """ + Create seeds for DataSectionHeader models. + """ + # Load preset fixtures. + app_fixtures.populate_data_section_headers() + + # Seed logic goes here. + + def create_data_section_items(self, model_count): + """ + Create seeds for DataSectionItem models. + """ + # Load preset fixtures. + app_fixtures.populate_data_section_items() + + # Seed logic goes here. diff --git a/resume_manager_core/models.py b/resume_manager_core/models.py index 71a836239075aa6e6e4ecb700e9c42c95c022d91..99c25fff05299e4f32b64624d6ffa69c80980f20 100644 --- a/resume_manager_core/models.py +++ b/resume_manager_core/models.py @@ -1,3 +1,175 @@ +""" +Models for ResumeManager app. +""" + +# Third-Party Imports. from django.db import models +from django.conf import settings +from django.core.exceptions import ValidationError +from django.utils import timezone + +# Internal Imports. +from core.models import WorkspaceModel + + +class Resume(WorkspaceModel): + """A representation of a resume.""" + + # Relationship Keys. + user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE) + theme = models.ForeignKey('ResumeTheme', on_delete=models.CASCADE) + + # Model fields. + name = models.CharField(max_length=settings.MAX_LENGTH) + last_activity = models.DateField(default=timezone.now) + + # Field helper text. + name.help_text = 'Resume name.' + last_activity.help_text = 'Last set date of resume updating. Defaults to creation date. Manually set after.' + + class Meta: + verbose_name = 'Resume' + verbose_name_plural = 'Resumes' + + def __str__(self): + return '{0} - {1}'.format(self.name, self.last_activity) + + +class ResumeTheme(WorkspaceModel): + """A unique resume theme.""" + + # Model fields. + name = models.CharField(max_length=settings.MAX_LENGTH) + css_classes = models.CharField(max_length=settings.MAX_LENGTH) + has_header_image = models.BooleanField(default=False) + has_footer_image = models.BooleanField(default=False) + + # Field helper text. + name.help_text = 'Name of theme.' + has_header_image.help_text = 'Indicates if theme includes header image.' + has_footer_image.help_text = 'Indicates if theme includes footer image.' + + class Meta: + verbose_name = 'Resume Theme' + verbose_name_plural = 'Resume Themes' + + def __str__(self): + return '{0}'.format(self.name) + + +class ResumeSection(WorkspaceModel): + """Formatting of a section/subsection within a resume.""" + # Model fields. + name = models.CharField(max_length=settings.MAX_LENGTH) + css_classes = models.CharField(max_length=settings.MAX_LENGTH) + + # Field helper text. + name.help_text = 'Name of resume section.' + css_classes.help_text = 'CSS classes to apply for section.' + + class Meta: + verbose_name = 'Resume Section' + verbose_name_plural = 'Resume Sections' + + def __str__(self): + return '{0}'.format(self.name) + + +class ResumeToSectionIntermediary(WorkspaceModel): + """An intermediary model that associates a resume and a section. + + Can be nested within other section/subsections, denoted by having a "parent". + Each subsection is a unique div. + """ + + # # Preset field choices. + # DISPLAY_CATEGORY__NONE = 0 + # DISPLAY_CATEGORY__EXPERIENCE = 1 + # DISPLAY_CATEGORY__EDUCATION = 2 + # DISPLAY_CATEGORY__SKILLS = 3 + # DISPLAY_CATEGORY_CHOICES = ( + # (DISPLAY_CATEGORY__NONE, 'None'), + # (DISPLAY_CATEGORY__EXPERIENCE, 'Experience'), + # (DISPLAY_CATEGORY__EDUCATION, 'Education'), + # (DISPLAY_CATEGORY__SKILLS, 'Skills'), + # ) + + # Relationship Keys. + resume = models.ForeignKey('Resume', on_delete=models.CASCADE) + parent_section = models.ForeignKey('self', on_delete=models.CASCADE, blank=True, null=True) + section_format = models.ForeignKey('ResumeSection', on_delete=models.CASCADE) + is_data_subsection = models.BooleanField(default=False) + # display_category = models.PositiveSmallIntegerField(choices=DISPLAY_CATEGORY_CHOICES, default=0) + + # Field helper text. + resume.help_text = 'The resume to place the section within.' + parent_section.help_text = 'Parent section/subsection to nest within.' + section_format.help_text = 'The section formatting to use within the resume.' + is_data_subsection.help_text = 'Indicates if this is part of a larger parent data section. Results in de-emphasized text.' + # display_category.help_text = 'Category of data to display in section.' + + class Meta: + verbose_name = 'Resume-to-Section Intermediary' + verbose_name_plural = 'Resume-to-Section Intermediaries' + + def __str__(self): + return '{0} - {1}'.format(self.resume, self.section_format) + + +# region Resume Data + +class DataSectionHeader(WorkspaceModel): + """Implementation of a header for a data section. + + Each data section can only have a single header, but one is not required. + """ + + # Relationship Keys. + resume_section = models.ForeignKey('ResumeToSectionIntermediary', on_delete=models.CASCADE) + + # Model fields. + section_name = models.CharField(max_length=settings.MAX_LENGTH) + header = models.TextField(blank=True) + + class Meta: + verbose_name = 'Data Section Header' + verbose_name_plural = 'Data Section Headers' + + def __str__(self): + return '{0} - {1}'.format(self.resume_section.resume, self.section_name) + + +class DataItem(WorkspaceModel): + """Abstract implementation of a data item. + + If a data section has only a single item, then the item is displayed in block/paragraph format. + If a data section has two or more items, then they are displayed in bullet point format. + """ + + # Relationship Keys. + resume_section = models.ForeignKey('ResumeToSectionIntermediary', on_delete=models.CASCADE) + + # Model fields. + section_name = models.CharField(max_length=settings.MAX_LENGTH) + item = models.TextField(blank=True) + is_emphasized = models.BooleanField(default=False) + is_minimized = models.BooleanField(default=False) + + class Meta: + verbose_name = 'Data Section Item' + verbose_name_plural = 'Data Section Items' + + def __str__(self): + return '{0} - {1}'.format(self.resume_section.resume, self.section_name) + + def clean(self): + """ + Custom cleaning implementation. Includes validation, setting fields, etc. + """ + # Call parent logic. + super().clean() + + if self.is_emphasized and self.is_minimized: + raise ValidationError('Data cannot be both emphasized and minimized in style.') -# Create your models here. +# endregion Resume Data