diff --git a/app/models/job.py b/app/models/job.py index db5a4fb..aff2bf6 100644 --- a/app/models/job.py +++ b/app/models/job.py @@ -68,4 +68,4 @@ class JobOfferTableModel(Base): @property def logo(self): - return self.file_associations[0].file_url if self.file_associations else None + return self.file_associations[0].url if self.file_associations else None diff --git a/app/seeds/__init__.py b/app/seeds/__init__.py index 9466579..08a146f 100644 --- a/app/seeds/__init__.py +++ b/app/seeds/__init__.py @@ -1,8 +1,19 @@ from .channels import seed_channels from .housing_offers import seed_housing_data from .interests import seed_interests +from .items import seed_items +from .job_offers import seed_job_offers from .reports import seed_reports from .university import seed_universities from .users import seed_users -__all__ = ["seed_interests", "seed_users", "seed_channels", "seed_housing_data", "seed_universities", "seed_reports"] +__all__ = [ + "seed_interests", + "seed_users", + "seed_channels", + "seed_housing_data", + "seed_universities", + "seed_reports", + "seed_items", + "seed_job_offers", +] diff --git a/app/seeds/items.py b/app/seeds/items.py new file mode 100644 index 0000000..ee5388e --- /dev/null +++ b/app/seeds/items.py @@ -0,0 +1,202 @@ +import datetime +import uuid +from pathlib import Path +from typing import List + +from sqlalchemy.orm import Session + +from app.literals.item import ItemCondition, ItemStatus +from app.models import File, User +from app.models.file_association import FileAssociation +from app.models.item import ItemTableModel +from app.models.item_category import ItemCategoryTableModel + + +def seed_items(db: Session, users: List[User]) -> List[ItemTableModel]: + """Create marketplace items with realistic data and static images.""" + + existing_items = db.query(ItemTableModel).first() + if existing_items: + items = db.query(ItemTableModel).limit(10).all() + print(f"* Items already seeded ({len(items)} items)") + return items + + print("Seeding marketplace items...") + + electronics = db.query(ItemCategoryTableModel).filter_by(name="Electronics").first() + furniture = db.query(ItemCategoryTableModel).filter_by(name="Furniture").first() + sports = db.query(ItemCategoryTableModel).filter_by(name="Sports").first() + other = db.query(ItemCategoryTableModel).filter_by(name="Other").first() + + if not electronics or not furniture or not sports or not other: + print("ERROR: Item categories not found. Make sure to run seed_item_categories() first.") + return [] + + verified_users = [u for u in users if u.is_verified] + if len(verified_users) < 3: + print("ERROR: Not enough verified users found.") + return [] + + static_images_path = Path(__file__).parent.parent / "static_photos" / "second-hand" + + items_data = [ + { + "title": 'MacBook Pro 13" 2020', + "description": ( + "Well-maintained MacBook Pro with M1 chip, 8GB RAM, 256GB SSD. Used for two years for university work. " + "Comes with original charger and protective case. Battery health at 87%. " + "Perfect for students studying Computer Science or Engineering!" + ), + "price": 799.00, + "currency": "EUR", + "location": "Lleida, España", + "condition": ItemCondition.GOOD, + "category": electronics, + "seller": verified_users[0], + "image_folder": "MacbookPro", + }, + { + "title": "Mountain Bike - Giant Talon", + "description": ( + 'Giant Talon mountain bike in excellent condition. 27.5" wheels, aluminum frame, Shimano gears. ' + "Perfect for exploring the Pyrenees on weekends! Recently serviced with new brake pads. " + "Ideal for students who love outdoor activities." + ), + "price": 450.00, + "currency": "EUR", + "location": "Lleida, España", + "condition": ItemCondition.GOOD, + "category": sports, + "seller": verified_users[1], + "image_folder": "Bike", + }, + { + "title": "Road Bike - Specialized Allez", + "description": ( + "Specialized Allez road bike in great condition. " + "Lightweight aluminum frame, carbon fork, Shimano 105 groupset. " + "Perfect for students who want to stay fit and explore Catalonia! " + "Includes bike lock and water bottle holder." + ), + "price": 520.00, + "currency": "EUR", + "location": "Lleida, España", + "condition": ItemCondition.GOOD, + "category": sports, + "seller": verified_users[2], + "image_folder": "Bike-2", + }, + { + "title": "Storage Cabinet / Bookshelf", + "description": ( + "Solid wood storage cabinet, perfect for dorm rooms or small apartments. " + "5 shelves providing ample space for books, decorations, and supplies. " + "Some minor wear but very sturdy. Must pick up in Lleida. Great for organizing your study space!" + ), + "price": 45.00, + "currency": "EUR", + "location": "Lleida, España", + "condition": ItemCondition.GOOD, + "category": furniture, + "seller": verified_users[0], + "image_folder": "Cabinet", + }, + { + "title": "Large Minion Plush Toy", + "description": ( + "Adorable large Minion plush toy, perfect for Despicable Me fans! " + "Great condition, super soft and cuddly. " + "Makes a fun decoration for your room or a nice gift. " + "Approximately 50cm tall. Smoke-free home." + ), + "price": 15.00, + "currency": "EUR", + "location": "Lleida, España", + "condition": ItemCondition.LIKE_NEW, + "category": other, + "seller": verified_users[1], + "image_folder": "MinionPlush", + }, + ] + + items = [] + admin_user = users[0] + + for item_data in items_data: + image_folder_name = item_data.pop("image_folder") + seller = item_data.pop("seller") + category = item_data.pop("category") + + item = ItemTableModel( + id=uuid.uuid4(), + seller_id=seller.id, + category_id=category.id, + status=ItemStatus.ACTIVE, + posted_date=datetime.datetime.now(datetime.UTC), + updated_at=datetime.datetime.now(datetime.UTC), + **item_data, + ) + + db.add(item) + db.flush() + + image_folder_path = static_images_path / image_folder_name + if image_folder_path.exists() and image_folder_path.is_dir(): + image_files = sorted( + [ + f + for f in image_folder_path.iterdir() + if f.is_file() and f.suffix.lower() in [".jpg", ".jpeg", ".png", ".gif", ".webp"] + ] + ) + + for order, image_path in enumerate(image_files): + with open(image_path, "rb") as f: + image_data = f.read() + + ext = image_path.suffix.lower() + content_type_map = { + ".jpg": "image/jpeg", + ".jpeg": "image/jpeg", + ".png": "image/png", + ".gif": "image/gif", + ".webp": "image/webp", + } + content_type = content_type_map.get(ext, "image/jpeg") + + file_record = File( + id=uuid.uuid4(), + filename=image_path.name, + content_type=content_type, + file_size=len(image_data), + uploaded_at=datetime.datetime.now(datetime.UTC), + is_public=True, + storage_type="database", + file_data=image_data, + uploader_id=admin_user.id, + ) + + db.add(file_record) + db.flush() + + file_association = FileAssociation( + id=uuid.uuid4(), + file_id=file_record.id, + entity_type="item", + entity_id=item.id, + order=order, + created_at=datetime.datetime.now(datetime.UTC), + ) + + db.add(file_association) + + print(f" - {item_data['title']}: {len(image_files)} images") + else: + print(f"WARNING: Image folder not found: {image_folder_path}") + + items.append(item) + + db.commit() + + print(f"* Created {len(items)} marketplace items with static images") + return items diff --git a/app/seeds/job_offers.py b/app/seeds/job_offers.py new file mode 100644 index 0000000..65d0999 --- /dev/null +++ b/app/seeds/job_offers.py @@ -0,0 +1,390 @@ +import datetime +import random +import uuid +from pathlib import Path +from typing import List + +from sqlalchemy.orm import Session + +from app.literals.job import JobCategory, JobType, JobWorkplace +from app.models import File, User +from app.models.file_association import FileAssociation +from app.models.job import JobApplication, JobOfferTableModel + + +def seed_job_offers(db: Session, users: List[User]) -> List[JobOfferTableModel]: + """Create job offers with realistic data.""" + + existing_jobs = db.query(JobOfferTableModel).first() + if existing_jobs: + jobs = db.query(JobOfferTableModel).limit(10).all() + print(f"* Job offers already seeded ({len(jobs)} jobs)") + return jobs + + print("Seeding job offers...") + + verified_users = [u for u in users if u.is_verified] + if len(verified_users) < 2: + print("ERROR: Not enough verified users found.") + return [] + + now = datetime.datetime.now(datetime.UTC) + logos_path = Path(__file__).parent.parent / "static_photos" / "job-logos" + + jobs_data = [ + { + "title": "Full-Stack Developer", + "description": """We are seeking a talented Full-Stack Developer to join our growing tech team in Lleida. + +Key Responsibilities: +• Develop and maintain web applications using modern frameworks +• Collaborate with UX/UI designers to implement responsive interfaces +• Write clean, maintainable, and efficient code +• Participate in code reviews and agile ceremonies + +Requirements: +• Bachelor's degree in Computer Science or related field +• Proficiency in React, Node.js, and PostgreSQL +• Strong problem-solving skills +• Fluency in Spanish and English + +We offer flexible working hours, professional development opportunities, and a dynamic work environment.""", + "category": JobCategory.TECHNOLOGY, + "job_type": JobType.FULL_TIME, + "workplace_type": JobWorkplace.HYBRID, + "location": "Lleida", + "salary_min": 28000, + "salary_max": 38000, + "salary_period": "year", + "company_name": "TechCat Solutions", + "company_description": ( + "Leading software development company in Catalonia " + "specializing in innovative digital solutions." + ), + "company_website": "https://techcat-solutions.example.com", + "company_employee_count": "50-100", + "user": verified_users[0], + "logo_file": "techcat-solutions.jpg", + "days_ago": 7, + }, + { + "title": "Marketing Intern", + "description": """Exciting internship opportunity for marketing students! + +Responsibilities: +• Assist in social media content creation and management +• Support email marketing campaigns +• Conduct market research and competitor analysis +• Help organize promotional events + +Ideal Candidate: +• Currently enrolled in Marketing, Business, or Communications degree +• Creative mindset with strong communication skills +• Familiarity with social media platforms +• Basic knowledge of Adobe Creative Suite is a plus + +This is a great opportunity to gain hands-on experience in a fast-paced startup environment!""", + "category": JobCategory.MARKETING, + "job_type": JobType.INTERNSHIP, + "workplace_type": JobWorkplace.ON_SITE, + "location": "Lleida", + "salary_min": 600, + "salary_max": 800, + "salary_period": "month", + "company_name": "DigitalBoost Agency", + "company_description": "Creative digital marketing agency helping local businesses grow online.", + "company_website": "https://digitalboost.example.com", + "company_employee_count": "10-25", + "user": verified_users[1], + "logo_file": "digitalboost-agency.jpg", + "days_ago": 3, + }, + { + "title": "Graphic Designer", + "description": """We're looking for a creative Graphic Designer to join our team! + +What You'll Do: +• Create visual content for web and print media +• Design marketing materials, brochures, and advertisements +• Collaborate with the marketing team on brand identity +• Develop layouts for social media campaigns + +Requirements: +• Portfolio demonstrating strong design skills +• Proficiency in Adobe Creative Suite (Photoshop, Illustrator, InDesign) +• Understanding of typography, color theory, and composition +• Ability to work on multiple projects simultaneously + +Work with a passionate team in a collaborative environment!""", + "category": JobCategory.DESIGN, + "job_type": JobType.PART_TIME, + "workplace_type": JobWorkplace.REMOTE, + "location": "Remote", + "salary_min": 1200, + "salary_max": 1800, + "salary_period": "month", + "company_name": "CreativeMinds Studio", + "company_description": "Boutique design studio specializing in branding and visual identity.", + "company_website": "https://creativeminds.example.com", + "company_employee_count": "5-10", + "user": verified_users[0], + "logo_file": "creativeminds-studio.jpg", + "days_ago": 14, + }, + { + "title": "Backend Software Engineer", + "description": """Join our engineering team to build scalable backend systems! + +Responsibilities: +• Design and implement RESTful APIs +• Optimize database queries and improve performance +• Write unit and integration tests +• Deploy and monitor microservices in cloud environments + +Requirements: +• 2+ years of experience with Python or Java +• Strong understanding of SQL and NoSQL databases +• Experience with Docker and CI/CD pipelines +• Knowledge of AWS or Azure cloud platforms + +Competitive salary, remote work options, and continuous learning opportunities!""", + "category": JobCategory.ENGINEERING, + "job_type": JobType.FULL_TIME, + "workplace_type": JobWorkplace.HYBRID, + "location": "Lleida", + "salary_min": 32000, + "salary_max": 45000, + "salary_period": "year", + "company_name": "DataFlow Systems", + "company_description": "Enterprise software company building data processing solutions.", + "company_website": "https://dataflow-systems.example.com", + "company_employee_count": "100-250", + "user": verified_users[1], + "logo_file": "dataflow-systems.jpg", + "days_ago": 5, + }, + { + "title": "Customer Support Specialist", + "description": """Help our customers succeed with our products! + +Responsibilities: +• Respond to customer inquiries via email, chat, and phone +• Troubleshoot technical issues and provide solutions +• Document customer feedback and report bugs +• Create help articles and FAQs + +Requirements: +• Excellent communication skills in Spanish and English +• Patient and empathetic approach to customer service +• Basic technical knowledge and willingness to learn +• Previous experience in customer support is a plus + +Flexible schedule and opportunities for career growth!""", + "category": JobCategory.CUSTOMER_SERVICE, + "job_type": JobType.FULL_TIME, + "workplace_type": JobWorkplace.ON_SITE, + "location": "Lleida", + "salary_min": 20000, + "salary_max": 26000, + "salary_period": "year", + "company_name": "HelpDesk Pro", + "company_description": "Customer service platform helping businesses deliver excellent support.", + "company_website": "https://helpdesk-pro.example.com", + "company_employee_count": "25-50", + "user": verified_users[0], + "logo_file": "helpdesk-pro.jpg", + "days_ago": 1, + }, + { + "title": "UX/UI Designer", + "description": """Design exceptional user experiences for web and mobile apps! + +Key Responsibilities: +• Conduct user research and usability testing +• Create wireframes, prototypes, and high-fidelity mockups +• Develop design systems and style guides +• Collaborate with developers to ensure design implementation + +Requirements: +• 1-3 years of UX/UI design experience +• Strong portfolio showcasing mobile and web projects +• Proficiency in Figma, Sketch, or Adobe XD +• Understanding of user-centered design principles + +Work on exciting projects with a talented design team!""", + "category": JobCategory.DESIGN, + "job_type": JobType.FULL_TIME, + "workplace_type": JobWorkplace.REMOTE, + "location": "Remote", + "salary_min": 26000, + "salary_max": 35000, + "salary_period": "year", + "company_name": "UserFirst Design", + "company_description": "UX consultancy helping companies create user-friendly products.", + "company_website": "https://userfirst.example.com", + "company_employee_count": "10-25", + "user": verified_users[1], + "logo_file": "userfirst-design.jpg", + "days_ago": 10, + }, + { + "title": "Data Analyst", + "description": """Turn data into actionable insights! + +Responsibilities: +• Analyze large datasets to identify trends and patterns +• Create dashboards and visualizations using Power BI/Tableau +• Collaborate with business teams to define KPIs +• Build predictive models and forecasts + +Requirements: +• Degree in Statistics, Mathematics, or related field +• Strong SQL skills and experience with Python/R +• Experience with data visualization tools +• Analytical mindset and attention to detail + +Great benefits package and professional development opportunities!""", + "category": JobCategory.TECHNOLOGY, + "job_type": JobType.FULL_TIME, + "workplace_type": JobWorkplace.HYBRID, + "location": "Lleida", + "salary_min": 30000, + "salary_max": 40000, + "salary_period": "year", + "company_name": "Analytics Hub", + "company_description": "Data analytics consulting firm serving clients across industries.", + "company_website": "https://analytics-hub.example.com", + "company_employee_count": "50-100", + "user": verified_users[0], + "logo_file": "analytics-hub.jpg", + "days_ago": 21, + }, + { + "title": "Content Writer", + "description": """Create engaging content for our blog and marketing materials! + +Responsibilities: +• Write blog posts, articles, and website copy +• Research industry trends and topics +• Optimize content for SEO +• Collaborate with the marketing team on content strategy + +Requirements: +• Excellent writing skills in Spanish and English +• Experience with content management systems (WordPress) +• Basic understanding of SEO best practices +• Portfolio of published articles + +Flexible remote work and creative freedom!""", + "category": JobCategory.MARKETING, + "job_type": JobType.FREELANCE, + "workplace_type": JobWorkplace.REMOTE, + "location": "Remote", + "salary_min": None, + "salary_max": None, + "salary_period": "project", + "company_name": "ContentCraft", + "company_description": None, + "company_website": None, + "company_employee_count": None, + "user": verified_users[1], + "logo_file": None, + "days_ago": 30, + }, + ] + + jobs_with_dates = [] + admin_user = users[0] + + for job_data in jobs_data: + logo_filename = job_data.pop("logo_file", None) + user = job_data.pop("user") + days_ago = job_data.pop("days_ago") + + created_at = now - datetime.timedelta(days=days_ago) + + job = JobOfferTableModel( + id=uuid.uuid4(), + user_id=user.id, + created_at=created_at, + is_active=True, + **job_data, + ) + + db.add(job) + db.flush() + + if logo_filename: + logo_path = logos_path / logo_filename + if logo_path.exists(): + with open(logo_path, "rb") as f: + logo_data = f.read() + + ext = logo_path.suffix.lower() + content_type_map = { + ".jpg": "image/jpeg", + ".jpeg": "image/jpeg", + ".png": "image/png", + } + content_type = content_type_map.get(ext, "image/jpeg") + + file_record = File( + id=uuid.uuid4(), + filename=logo_filename, + content_type=content_type, + file_size=len(logo_data), + uploaded_at=created_at, + is_public=True, + storage_type="database", + file_data=logo_data, + uploader_id=admin_user.id, + ) + + db.add(file_record) + db.flush() + + file_association = FileAssociation( + id=uuid.uuid4(), + file_id=file_record.id, + entity_type="job_offer", + entity_id=job.id, + order=0, + created_at=created_at, + ) + + db.add(file_association) + else: + print(f"WARNING: Logo file not found: {logo_path}") + + jobs_with_dates.append((job, created_at)) + + db.commit() + + applicant_pool = [u for u in verified_users if u.role != "Admin"][:5] + + if applicant_pool: + applications_created = 0 + for job, job_created_at in jobs_with_dates: + num_applications = random.choice([0, 0, 1, 2, 3, 4]) + + if num_applications > 0: + applicants = random.sample(applicant_pool, min(num_applications, len(applicant_pool))) + for applicant in applicants: + job_age_days = int((now - job_created_at).total_seconds() / 86400) + days_after_posting = random.randint(0, min(7, job_age_days)) + applied_at = job_created_at + datetime.timedelta(days=days_after_posting) + + application = JobApplication( + user_id=applicant.id, + job_id=job.id, + applied_at=applied_at, + ) + db.add(application) + applications_created += 1 + + db.commit() + print(f"* Created {len(jobs_with_dates)} job offers with {applications_created} applications") + else: + print(f"* Created {len(jobs_with_dates)} job offers") + + return [job for job, _ in jobs_with_dates] diff --git a/app/seeds/seed.py b/app/seeds/seed.py index 9f65f16..437416b 100644 --- a/app/seeds/seed.py +++ b/app/seeds/seed.py @@ -10,6 +10,8 @@ from app.seeds.conversations import seed_conversations from app.seeds.interests import seed_interests from app.seeds.item_category import seed_item_categories +from app.seeds.items import seed_items +from app.seeds.job_offers import seed_job_offers from app.seeds.messages import seed_messages from app.seeds.reports import seed_reports from app.seeds.terms import seed_terms @@ -51,6 +53,12 @@ def seed_database(nuke: bool = False): seed_item_categories(db) print("- Marketplace Item Categories seeded") + seed_items(db, users) + print("- Marketplace Items seeded") + + seed_job_offers(db, users) + print("- Job Offers seeded") + terms = seed_terms(db) print("- Terms seeded") diff --git a/app/seeds/terms.py b/app/seeds/terms.py index f17e0ef..af538e4 100644 --- a/app/seeds/terms.py +++ b/app/seeds/terms.py @@ -1,42 +1,89 @@ import datetime +import json from sqlalchemy.orm import Session from app.models import TermsTableModel -# Sample Terms & Conditions content -TERMS_CONTENT_V1 = """ -Welcome to the UniHub app! +TERMS_CONTENT_V1 = { + "ca": """
Darrera actualització: 22 de desembre de 2025
-1. General provisions - By using the app, you accept the rules of the student community. -2. Conduct guidelines - Be nice to other students. Do not spam on channels. -3. Housing offers - Post only genuine listings. -4. Privacy - Your data is safe (usually). +Aquest lloc web és operat per [Nom de l'Empresa]. " + "L'accés i l'ús d'aquesta pàgina atribueix la condició d'usuari " + "i implica l'acceptació plena d'aquestes condicions.
-Version 1.0 -""" +Tots els continguts, textos, imatges i codi font són propietat de [Nom de l'Empresa] " + "o de tercers amb autorització. " + "Queda prohibida la seva reproducció sense permís previ.
+ +No ens fem responsables dels danys derivats d'interrupcions del servei, " + "virus informàtics o un mal ús del contingut per part de l'usuari.
+ +Aquests termes es regeixen per la normativa vigent i qualsevol litigi se sotmetrà als tribunals de [Ciutat].
""", + "es": """Última actualización: 22 de diciembre de 2025
+ +Este sitio web es operado por [Nombre de la Empresa]. " + "El acceso y uso de esta página le atribuye la condición de usuario " + "e implica la aceptación plena de estas condiciones.
+ +Todos los contenidos, textos, imágenes y código fuente son propiedad de [Nombre de la Empresa] " + "o de terceros con autorización. " + "Queda prohibida su reproducción sin permiso previo.
+ +No nos hacemos responsables de los daños derivados de interrupciones del servicio, " + "virus informáticos o un mal uso del contenido por parte del usuario.
+ +Estos términos se rigen por la normativa vigente y cualquier litigio " + "se someterá a los tribunales de [Ciudad].
""", + "en": """Last updated: December 22, 2025
+ +This website is operated by [Company Name]. " + "By accessing and using this site, you accept and agree to be bound " + "by these terms and conditions.
+ +All content, including text, images, and source code, is the property of [Company Name] " + "or authorized third parties. " + "Reproduction without prior permission is prohibited.
+ +We are not liable for damages resulting from service interruptions, " + "computer viruses, or improper use of the content by the user.
+ +These terms are governed by current legislation, and any disputes will be submitted to the courts of [City].
""", +} def seed_terms(db: Session) -> TermsTableModel: - """Creates default Terms and Conditions version.""" + """Creates default Terms and Conditions version with multilingual content.""" - # Check if version 1.0.0 already exists - existing_terms = db.query(TermsTableModel).filter_by(version="1.0.0").first() + existing_terms = db.query(TermsTableModel).filter_by(version="v1.0.0").first() if existing_terms: - print(f"* Terms 1.0.0 already exists ({existing_terms.id})") + print(f"* Terms v1.0.0 already exists ({existing_terms.id})") return existing_terms - print("Seeding Terms 1.0.0...") + print("Seeding Terms v1.0.0...") + + content_json = json.dumps(TERMS_CONTENT_V1) - terms = TermsTableModel(version="1.0.0", content=TERMS_CONTENT_V1, created_at=datetime.datetime.now(datetime.UTC)) + terms = TermsTableModel(version="v1.0.0", content=content_json, created_at=datetime.datetime.now(datetime.UTC)) db.add(terms) db.commit() db.refresh(terms) - print(f"* Created Terms 1.0.0 with ID: {terms.id}") + print(f"* Created Terms v1.0.0 with ID: {terms.id}") return terms diff --git a/app/static_photos/job-logos/analytics-hub.jpg b/app/static_photos/job-logos/analytics-hub.jpg new file mode 100644 index 0000000..8b7a730 Binary files /dev/null and b/app/static_photos/job-logos/analytics-hub.jpg differ diff --git a/app/static_photos/job-logos/creativeminds-studio.jpg b/app/static_photos/job-logos/creativeminds-studio.jpg new file mode 100644 index 0000000..828a4fc Binary files /dev/null and b/app/static_photos/job-logos/creativeminds-studio.jpg differ diff --git a/app/static_photos/job-logos/dataflow-systems.jpg b/app/static_photos/job-logos/dataflow-systems.jpg new file mode 100644 index 0000000..03ac133 Binary files /dev/null and b/app/static_photos/job-logos/dataflow-systems.jpg differ diff --git a/app/static_photos/job-logos/digitalboost-agency.jpg b/app/static_photos/job-logos/digitalboost-agency.jpg new file mode 100644 index 0000000..7683503 Binary files /dev/null and b/app/static_photos/job-logos/digitalboost-agency.jpg differ diff --git a/app/static_photos/job-logos/helpdesk-pro.jpg b/app/static_photos/job-logos/helpdesk-pro.jpg new file mode 100644 index 0000000..afbb67d Binary files /dev/null and b/app/static_photos/job-logos/helpdesk-pro.jpg differ diff --git a/app/static_photos/job-logos/techcat-solutions.jpg b/app/static_photos/job-logos/techcat-solutions.jpg new file mode 100644 index 0000000..9aa86e0 Binary files /dev/null and b/app/static_photos/job-logos/techcat-solutions.jpg differ diff --git a/app/static_photos/job-logos/userfirst-design.jpg b/app/static_photos/job-logos/userfirst-design.jpg new file mode 100644 index 0000000..80de3b5 Binary files /dev/null and b/app/static_photos/job-logos/userfirst-design.jpg differ diff --git a/app/static_photos/second-hand/Bike-2/Bike-1.jpg b/app/static_photos/second-hand/Bike-2/Bike-1.jpg new file mode 100644 index 0000000..9b35277 Binary files /dev/null and b/app/static_photos/second-hand/Bike-2/Bike-1.jpg differ diff --git a/app/static_photos/second-hand/Bike-2/Bike-2.jpg b/app/static_photos/second-hand/Bike-2/Bike-2.jpg new file mode 100644 index 0000000..c4d4047 Binary files /dev/null and b/app/static_photos/second-hand/Bike-2/Bike-2.jpg differ diff --git a/app/static_photos/second-hand/Bike-2/Bike-3.jpg b/app/static_photos/second-hand/Bike-2/Bike-3.jpg new file mode 100644 index 0000000..4a70fa7 Binary files /dev/null and b/app/static_photos/second-hand/Bike-2/Bike-3.jpg differ diff --git a/app/static_photos/second-hand/Bike-2/Bike-4.jpg b/app/static_photos/second-hand/Bike-2/Bike-4.jpg new file mode 100644 index 0000000..3289ab8 Binary files /dev/null and b/app/static_photos/second-hand/Bike-2/Bike-4.jpg differ diff --git a/app/static_photos/second-hand/Bike-2/Bike-5.jpg b/app/static_photos/second-hand/Bike-2/Bike-5.jpg new file mode 100644 index 0000000..64a8177 Binary files /dev/null and b/app/static_photos/second-hand/Bike-2/Bike-5.jpg differ diff --git a/app/static_photos/second-hand/Bike-2/Bike-6.jpg b/app/static_photos/second-hand/Bike-2/Bike-6.jpg new file mode 100644 index 0000000..372c7ec Binary files /dev/null and b/app/static_photos/second-hand/Bike-2/Bike-6.jpg differ diff --git a/app/static_photos/second-hand/Bike-2/Bike-7.jpg b/app/static_photos/second-hand/Bike-2/Bike-7.jpg new file mode 100644 index 0000000..ecfd017 Binary files /dev/null and b/app/static_photos/second-hand/Bike-2/Bike-7.jpg differ diff --git a/app/static_photos/second-hand/Bike-2/Bike-8.jpg b/app/static_photos/second-hand/Bike-2/Bike-8.jpg new file mode 100644 index 0000000..1de9f9d Binary files /dev/null and b/app/static_photos/second-hand/Bike-2/Bike-8.jpg differ diff --git a/app/static_photos/second-hand/Bike-2/Bike-9.jpg b/app/static_photos/second-hand/Bike-2/Bike-9.jpg new file mode 100644 index 0000000..95b9bb8 Binary files /dev/null and b/app/static_photos/second-hand/Bike-2/Bike-9.jpg differ diff --git a/app/static_photos/second-hand/Bike/Bike-1.jpg b/app/static_photos/second-hand/Bike/Bike-1.jpg new file mode 100644 index 0000000..12c41a1 Binary files /dev/null and b/app/static_photos/second-hand/Bike/Bike-1.jpg differ diff --git a/app/static_photos/second-hand/Bike/Bike-2.jpg b/app/static_photos/second-hand/Bike/Bike-2.jpg new file mode 100644 index 0000000..b717721 Binary files /dev/null and b/app/static_photos/second-hand/Bike/Bike-2.jpg differ diff --git a/app/static_photos/second-hand/Bike/Bike-3.jpg b/app/static_photos/second-hand/Bike/Bike-3.jpg new file mode 100644 index 0000000..d8bb4fd Binary files /dev/null and b/app/static_photos/second-hand/Bike/Bike-3.jpg differ diff --git a/app/static_photos/second-hand/Bike/Bike-4.jpg b/app/static_photos/second-hand/Bike/Bike-4.jpg new file mode 100644 index 0000000..1cb5a8e Binary files /dev/null and b/app/static_photos/second-hand/Bike/Bike-4.jpg differ diff --git a/app/static_photos/second-hand/Bike/Bike-5.jpg b/app/static_photos/second-hand/Bike/Bike-5.jpg new file mode 100644 index 0000000..960d415 Binary files /dev/null and b/app/static_photos/second-hand/Bike/Bike-5.jpg differ diff --git a/app/static_photos/second-hand/Bike/Bike-6.jpg b/app/static_photos/second-hand/Bike/Bike-6.jpg new file mode 100644 index 0000000..32f573f Binary files /dev/null and b/app/static_photos/second-hand/Bike/Bike-6.jpg differ diff --git a/app/static_photos/second-hand/Cabinet/Cabinet-1.jpg b/app/static_photos/second-hand/Cabinet/Cabinet-1.jpg new file mode 100644 index 0000000..b84a451 Binary files /dev/null and b/app/static_photos/second-hand/Cabinet/Cabinet-1.jpg differ diff --git a/app/static_photos/second-hand/Cabinet/Cabinet-2.jpg b/app/static_photos/second-hand/Cabinet/Cabinet-2.jpg new file mode 100644 index 0000000..ed9592e Binary files /dev/null and b/app/static_photos/second-hand/Cabinet/Cabinet-2.jpg differ diff --git a/app/static_photos/second-hand/Cabinet/Cabinet-3.jpg b/app/static_photos/second-hand/Cabinet/Cabinet-3.jpg new file mode 100644 index 0000000..b8042aa Binary files /dev/null and b/app/static_photos/second-hand/Cabinet/Cabinet-3.jpg differ diff --git a/app/static_photos/second-hand/MacbookPro/MacbookPro-1.jpg b/app/static_photos/second-hand/MacbookPro/MacbookPro-1.jpg new file mode 100644 index 0000000..a85e6cf Binary files /dev/null and b/app/static_photos/second-hand/MacbookPro/MacbookPro-1.jpg differ diff --git a/app/static_photos/second-hand/MacbookPro/MacbookPro-2.jpg b/app/static_photos/second-hand/MacbookPro/MacbookPro-2.jpg new file mode 100644 index 0000000..de209c1 Binary files /dev/null and b/app/static_photos/second-hand/MacbookPro/MacbookPro-2.jpg differ diff --git a/app/static_photos/second-hand/MinionPlush/Minion-1.jpg b/app/static_photos/second-hand/MinionPlush/Minion-1.jpg new file mode 100644 index 0000000..25f4851 Binary files /dev/null and b/app/static_photos/second-hand/MinionPlush/Minion-1.jpg differ diff --git a/app/static_photos/second-hand/MinionPlush/Minion-2.jpg b/app/static_photos/second-hand/MinionPlush/Minion-2.jpg new file mode 100644 index 0000000..cc6bf04 Binary files /dev/null and b/app/static_photos/second-hand/MinionPlush/Minion-2.jpg differ diff --git a/app/static_photos/second-hand/MinionPlush/Minion-3.jpg b/app/static_photos/second-hand/MinionPlush/Minion-3.jpg new file mode 100644 index 0000000..e0d7ada Binary files /dev/null and b/app/static_photos/second-hand/MinionPlush/Minion-3.jpg differ diff --git a/scripts/check_migration_included.py b/scripts/check_migration_included.py index 5a48953..7ced3a6 100644 --- a/scripts/check_migration_included.py +++ b/scripts/check_migration_included.py @@ -1,24 +1,77 @@ +import re import subprocess import sys +def is_schema_change(diff_line: str) -> bool: + """Check if a diff line represents a database schema change.""" + line = diff_line.strip() + + if not line.startswith(("+", "-")): + return False + + line_content = line[1:].strip() + + if not line_content or line_content.startswith("#"): + return False + + schema_patterns = [ + r"mapped_column\(", + r"__tablename__\s*=", + r"Mapped\[.*\]\s*=\s*mapped_column", + r"ForeignKey\(", + r"Index\(", + r"UniqueConstraint\(", + r"CheckConstraint\(", + r"Column\(", + r"Table\(", + ] + + for pattern in schema_patterns: + if re.search(pattern, line_content): + return True + + return False + + def main(): - """Check if model changes are accompanied by a migration file.""" + """Check if model changes require a migration file.""" result = subprocess.run( ["git", "diff", "--cached", "--name-only"], capture_output=True, text=True, check=True, ) - staged_files = result.stdout + staged_files = result.stdout.strip().split("\n") + + model_files = [f for f in staged_files if f.startswith("app/models/") and f.endswith(".py")] + + if not model_files: + sys.exit(0) + + result = subprocess.run( + ["git", "diff", "--cached"] + model_files, + capture_output=True, + text=True, + check=True, + ) + diff_content = result.stdout + + has_schema_changes = any(is_schema_change(line) for line in diff_content.split("\n")) - model_changed = "app/models/" in staged_files - migration_included = "migrations/versions/" in staged_files + if has_schema_changes: + migration_included = any(f.startswith("migrations/versions/") for f in staged_files) - if model_changed and not migration_included: - print("ERROR: Model files changed but no migration included!") - print("Run: alembic revision --autogenerate -m 'your message'") - sys.exit(1) + if not migration_included: + print("ERROR: Model schema changes detected but no migration included!") + print("Changes that require migrations:") + print(" - New or modified mapped_column definitions") + print(" - Table name changes") + print(" - Foreign key changes") + print(" - Index or constraint changes") + print() + print("Run: alembic revision --autogenerate -m 'your message'") + sys.exit(1) sys.exit(0) diff --git a/tests/test_email_verification.py b/tests/test_email_verification.py index 733d39e..13355f5 100644 --- a/tests/test_email_verification.py +++ b/tests/test_email_verification.py @@ -15,7 +15,7 @@ def test_send_verification_email_success(self, client, db): "password": "TestPass123!", "first_name": "Verify", "last_name": "Test", - "accepted_terms_version": "1.0.0", + "accepted_terms_version": "v1.0.0", }, ) @@ -114,7 +114,7 @@ def test_change_password_success(self, client, db): "password": "OldPass123!", "first_name": "PwChange", "last_name": "User", - "accepted_terms_version": "1.0.0", + "accepted_terms_version": "v1.0.0", }, ) assert signup_response.status_code == 201 @@ -175,7 +175,7 @@ def test_change_password_weak_password(self, client, db): "password": "StrongPass123!", "first_name": "Weak", "last_name": "Password", - "accepted_terms_version": "1.0.0", + "accepted_terms_version": "v1.0.0", }, ) token = signup_response.json()["access_token"]