diff --git a/config/django/base.py b/config/django/base.py index b153b0e..10bacb8 100644 --- a/config/django/base.py +++ b/config/django/base.py @@ -19,6 +19,7 @@ INSTALLED_APPS = [ "rest_framework_api_key", 'socials', 'events', + 'contracts', 'config', # 'academia_nuts', # 'leg_info', diff --git a/config/urls.py b/config/urls.py index aa92a15..c7408e6 100644 --- a/config/urls.py +++ b/config/urls.py @@ -23,6 +23,7 @@ from django.urls import path, include urlpatterns = [ path('socials/', include('socials.urls')), path('events/', include('events.urls')), + path('contracts/', include('contracts.urls')), path('digimon/', admin.site.urls), ] + static (settings.MEDIA_URL, document_root = settings.MEDIA_ROOT) # + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) diff --git a/contracts/__init__.py b/contracts/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/contracts/admin.py b/contracts/admin.py new file mode 100644 index 0000000..443dfbe --- /dev/null +++ b/contracts/admin.py @@ -0,0 +1,15 @@ +from django.contrib import admin +from .models import * + +class ContractAdmin(admin.ModelAdmin): +# prepopulated_fields = {"slug": ("shortname",)} + list_display = ("notice_id", "pub_date") + + + +# Register your models here. +admin.site.register(Paragraph) +admin.site.register(OriginalContract) +admin.site.register(Contract, ContractAdmin) +admin.site.register(Company) + diff --git a/contracts/apps.py b/contracts/apps.py new file mode 100644 index 0000000..82de8d1 --- /dev/null +++ b/contracts/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class ContractsConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'contracts' diff --git a/contracts/digitools.py b/contracts/digitools.py new file mode 100644 index 0000000..a09055a --- /dev/null +++ b/contracts/digitools.py @@ -0,0 +1,415 @@ +import os, sys +from datetime import datetime, timedelta +from dateutil import relativedelta +from time import sleep +import pytz +from lxml import html +from pprint import pprint as ppr + +import django + +from xvfbwrapper import Xvfb +from selenium import webdriver as wd + +from events.models import Event as DSEvent, Organization, Promo, Scraper, Calendar + +# tz = pytz.timezone("US/Central") +plus_one_month = relativedelta.relativedelta(months=1) +odt_next_month = datetime.now() + plus_one_month + +def translateMonth(month): + MONTHS = [ + ('Januar', 'JÄN', 'January'), + ('Februar', 'FEBR', 'February'), + ('März', 'MRZ', 'March'), + ('April', 'APR', 'April'), + ('Mai', 'MAI', 'May'), + ('Juni', 'JUN', 'June'), + ('Juli', 'JUL', 'July'), + ('August', 'AUG', 'August'), + ('September', 'SEP', 'September'), + ('Oktober', 'OKT', 'October'), + ('November', 'NOV', 'November'), + ('Dezember', 'DEZ', 'December'), + ] + for mon in MONTHS: + if month == mon[1]: + return mon[2] + +# Get Scraper name, item count and online_calendar (virtcal) +def getScraper(venue, website, cal): + virtcal = Calendar.objects.get(shortcode='000') + ncal = Calendar.objects.get(shortcode=cal) + try: + scraper, created = Scraper.objects.get_or_create( + name=venue.name, + website=website, + calendar = ncal, + items = 0, + new_items = 0, + last_ran = datetime.now(), + ) + venue.cal = ncal + venue.save() + except Exception as e: + print(e) + scraper = Scraper.objects.get(name=venue.name) + num_of_events = DSEvent.objects.filter(scraper=scraper) + scraper.items = len(num_of_events) + scraper.save() + print("Scraper: ", scraper) + return scraper, scraper.items, virtcal + +# Update item_count of the Scraper at the end of the scrape +def updateScraper(scraper, item_count_start): + num_of_events = DSEvent.objects.filter(scraper=scraper) + scraper.items = len(num_of_events) + scraper.new_items = len(num_of_events) - item_count_start + scraper.last_ran = datetime.now() + scraper.save() + print("Scaper Updated") + return + +# Get site HTML content for XPATH travel +def getSource(browser, link): + browser.get(link) + sleep(6) + ps = html.fromstring(browser.page_source) + return ps + +# Get Selenium Web Driver, with params for Chrome or Firefox +# Or in production to run headless +def getBrowser(run_env): + if run_env == 'dev': + print("Chrome is a go!") + br = wd.Chrome() + return br + elif run_env == "def": + print("Firefox go vroom") + br = wd.Firefox() + return br + elif run_env == "prod": + start_cmd = "Xvfb :91 && export DISPLAY=:91 &" + xvfb = Xvfb() + os.system(start_cmd) + xvfb.start() + print("started Xvfb") + br = wd.Firefox() + return br + else: + print("Failed", sys.argv, arg1) + quit() + +# Create Dated URL with zero-padded numbers +def createBasicURL(site_url): + month = datetime.now().month + next_month = odt_next_month.month + year = datetime.now().year + if next_month == 1: + next_year = year+1 + links = [ + site_url + str(month) + "/" + str(year), + site_url + str(next_month) + "/" + str(next_year) + ] + else: + links = [ + site_url + str(month) + "/" + str(year), + site_url + str(next_month) + "/" + str(year) + ] + return links + +# Create Dated URL without zero-padded numbers +def createURLNoZero(site_url): + month = datetime.now().month + next_month = odt_next_month.month + year = datetime.now().year + links = [ + site_url + str(year) + "/" + str(month), + ] + if next_month == "1": + links.append(site_url + str(int(year)+1) + "/" + str(next_month)) + else: + links.append(site_url + str(year) + "/" + str(next_month)) + return links + +# Create Dated URL Link with zero-padding +def createURL(site_url): + month = datetime.now().month + if month < 10: + month = "0" + str(month) + else: + month = str(month) + next_month = odt_next_month.month + if next_month < 10: + next_month = "0" + str(next_month) + else: + next_month = str(next_month) + year = datetime.now().year + links = [ + site_url + str(year) + "/" + month, + ] + if next_month == "01": + links.append(site_url + str(int(year)+1) + "/" + next_month) + else: + links.append(site_url + str(year) + "/" + next_month) + return links + +# Create Dated URL with dashes +def createDashURL(site_url): + month = datetime.now().month + if month < 10: + month = "0" + str(month) + else: + month = str(month) + next_month = odt.month + if next_month < 10: + next_month = "0" + str(next_month) + else: + next_month = str(next_month) + year = datetime.now().year + links = [ + site_url + month + "-" + str(year), + site_url + next_month + "-" + str(year) + ] + print(links) + return links + +# Add Calendar to Event Object (maybe extraneous) +def add_calendar(event, calendar): + # print("Add Calendar", type(event), event, calendar) + if type(event) is tuple: + event = event[0] + cal = Calendar.objects.get(shortcode=calendar) + event.calendar.add(cal) + event.save() + return event + +# Add Calendars to Event Object ?? +def add_calendars(event, data): + if type(data['calendars']) is not list: + event.calendar.add(data['calendars']) + else: + for cal in data['calendars']: + event.calendar.add(cal) + event.save() + return event + +# Create Basic DigiSnaxx Event +def createBasicEvent(event, event_type, venue): + try: + new_event, created = DSEvent.objects.update_or_create( + event_type = event_type, + show_title = event['title'], + show_link = event['link'], + show_date = event['dateStamp'], + scraper = event['scraper'], + venue = venue + ) + new_event = add_calendars(new_event, event) + print("\n+new event+") + return new_event, created + except Exception as e: + print("DT Error: ", e) + ppr(event) + return None, None + +# Create iCal Event +def createBasiciCalEvent(event, event_type, venue): + new_event, created = DSEvent.objects.update_or_create( + event_type = event_type, + show_title = event['title'][0], + show_link = event['link'], + show_date = datetime.strptime(str(event['dateStamp'][0]), '%Y-%m-%d %H:%M:%S%z %Z'), + scraper = event['scraper'], + venue = venue + ) + new_event = add_calendars(new_event, event) + print("Success") + return new_event, created + +def createDetailedEvent2(event, event_type, venue, scraper): + new_event, created = DSEvent.objects.update_or_create( + event_type = event_type, + show_title = event["show_title"], + show_link = event["link"], + show_date = event["dateStamp"], + more_details = event["details"], + scraper = event['scraper'], + venue = venue + ) + new_event = add_calendars(new_event, event) + print("Success") + return new_event, created + +# Create Detailed Event with Details & Guests +# Details in JSON Format +def createDetailedEvent(event, event_type, venue, scraper): + new_event, created = DSEvent.objects.update_or_create( + event_type = event_type, + show_title = event["show_title"], + show_link = event["link"], + show_date = event["dateStamp"], + guests = " ".join(event["guests"]), + more_details = event["details"], + scraper = event['scraper'], + venue = venue + ) + new_event = add_calendars(new_event, event) + print("Success") + return new_event, created + +# Create iCal event from DF_Online & Medellin +def createCleanIcalEvent(event, scraper, venue, event_type): + new_date = event['eventDate'] + new_event = {} + new_event['scraper'] = scraper + new_event['calendars'] = scraper.calendar + new_event['title'] = event['strSummary'], + new_event['date'] = str(new_date), + new_event['dateStamp'] = str(new_date), + new_event['link'] = venue.website + print("New Event") +# ppr(new_event) + createBasiciCalEvent(new_event, event_type, venue) + +# Get events from iCal +def getiCalEvents(gcal, scraper, venue, event_type): + events = [] + for component in gcal.walk(): + event = {} + event['scraper'] = scraper + event['calendars'] = [scraper.calendar] + event['strSummary'] = f"{(component.get('SUMMARY'))}" + event['strDesc'] = component.get('DESCRIPTION') + event['strLocation'] = component.get('LOCATION') + event['dateStart'] = component.get('DTSTART') + event['dateStamp'] = component.get('DTSTAMP') + if event['dateStamp'] is not None: + event['dateStamp'] = event['dateStamp'].dt + if event['dateStart'] is not None: + try: + event['dateStart'] = event['dateStart'].dt + except Exception as e: + print("what? ", e) + + if event['strSummary'] != 'None': + event['details'] = { + "description" : event['strDesc'], + "Location" : event['strLocation'], + } + events.append(event) + return events + +# Build iCal Events and Send to Create +def buildiCalEvents(events, event_type, scraper, venue): + for event in events: + e = {} + e['calendars'] = event['calendars'] + try: + e['dateStamp'] = event['dateStart'][0] + except: + e['dateStamp'] = event['dateStart'] + e['title'] = event['strSummary'] + e['scraper'] = scraper + e['link'] = venue.website + try: + createBasicEvent(e, event_type, venue) + scraper.items+=1 + except Exception as e: + print("Error: ", e) + scraper.save() + return + +def getMDEVenue(venue, event): + if venue.name == "DANCEFREE": + venue.website = "https://www.instagram.com/dancefreeco" + if venue.name == "Vintrash": + venue.website = "https://www.instagram.com/vintrashbar" + if venue.name == "The Wandering Paisa": + venue.website = "https://wanderingpaisahostel.com" + if venue.name == "Dulce Posion": + venue.website = "https://www.instagram.com/dulceposionr" + if venue.name == "Blood Dance Company": + venue.website = "https://www.instagram.com/blooddancecompany" + if venue.name == "OLSA Certified Spanish School": + venue.website = "https://www.olsafoundation.org/" + if event['strSummary'] == "Merli Rooftop Language Exchange": + venue.website = "https://calendar.google.com/calendar/embed?src=46ae0446724b1b3ee83cbd7dbc0db6a235bf97509ad860ca91eada3c267b5e41%40group.calendar.google.com&ctz=America%2FBogota" + if "Concious Warrior" in event['strSummary']: + venue.website = "https://www.consciouscolombia.com/" + venue.save() + print(venue) + return + +# Get iCal events for Medellin & OnlineEvents +def getiCalRepeateEvents(gcal, scraper, venue, event_type, cal): + for component in gcal.walk(): + event = {} + event['scraper'] = scraper + event['calendars'] = [scraper.calendar] + event['strSummary'] = f"{(component.get('SUMMARY'))}" + event['strDesc'] = component.get('DESCRIPTION') + event['strLocation'] = str(component.get('LOCATION')) + event['dateStart'] = component.get('DTSTART') + event['dateStamp'] = component.get('DTSTAMP') + if event['strSummary'] != 'None': + event['details'] = { + "description" : event['strDesc'], + "Location" : event['strLocation'], + } + if event['dateStamp'] != None: + event['dateStart'] = event['dateStart'].dt + event['dateStart'] = datetime.strptime(str(event['dateStart']) + " UTC", '%Y-%m-%d %H:%M:%S%z %Z') + event['timezone'] = str(event['dateStart'])[-6:].strip() + rules = component.get('RRule') + try: + if rules['FREQ'][0] == 'WEEKLY': + date = datetime.today().date() - timedelta(days=datetime.today().weekday()) + date = datetime.combine(date, event['dateStart'].time()) + days = ["SU", "MO", "TU", "WE", "TH", "FR", "SA"] + event = splitLocation(event, city="Medellin") + for day in rules['BYDAY']: + nday = days.index(day) + if cal == 'mde': + getMDEVenue(event['venue'],event) + print(event) + iCalEventRepeatFilter(nday, date, event, scraper, event['venue'], "Ed") + + except Exception as e: + print("Error: ", e, "\n\n") +# ppr(event) + pass + +def iCalEventRepeatFilter(day, date, event, scraper, venue, event_type): + print("repeate events") + days = [day-1, day+6, day+13] + for day in days: + event['dateStamp'] = date + timedelta(days=day) + dateStart = str(event['dateStamp']) + event['timezone'] + ' UTC' + event['eventDate'] = dateStart + createCleanIcalEvent(event, scraper, venue, event_type) + return + +def splitLocation(event, **kwargs): + loc_split = event['strLocation'].split(',') +# ppr(loc_split) + venue_name = loc_split[0] + venue, created = Organization.objects.get_or_create( + name=venue_name, + ) + event['venue'] = venue + if kwargs['city']: + venue.city = kwargs['city'] + venue.save() + return event + +# ARCHIVED Methods +def createBasicArticle(article, event_type, organization): + new_article, created = Promo.objects.update_or_create( + promo_type = 'Ja', + title = article['title'], + target_link = article['link'], + published = True, + organization = organization + ) + return new_article, created diff --git a/contracts/migrations/0001_initial.py b/contracts/migrations/0001_initial.py new file mode 100644 index 0000000..aa86318 --- /dev/null +++ b/contracts/migrations/0001_initial.py @@ -0,0 +1,95 @@ +# Generated by Django 6.0.1 on 2026-02-18 19:41 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='Tags', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=31, unique=True)), + ('desc', models.TextField(blank=True, null=True)), + ], + ), + migrations.CreateModel( + name='Company', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=63)), + ('unq_entity_id', models.CharField(blank=True, max_length=63, null=True)), + ('website', models.URLField(blank=True, max_length=127, null=True)), + ('short_desc', models.CharField(blank=True, max_length=63, null=True)), + ('long_desc', models.TextField(blank=True, null=True)), + ('gmap_link', models.CharField(blank=True, max_length=253, null=True)), + ('address_complete', models.CharField(blank=True, max_length=127, null=True)), + ('address_numbers', models.CharField(blank=True, max_length=63, null=True)), + ('address_type', models.CharField(blank=True, max_length=31, null=True)), + ('city', models.CharField(blank=True, max_length=127, null=True)), + ('state', models.CharField(blank=True, max_length=127, null=True)), + ('zip_code', models.CharField(blank=True, max_length=15, null=True)), + ('tags', models.ManyToManyField(blank=True, to='contracts.tags')), + ], + options={ + 'verbose_name_plural': 'Companies', + 'ordering': ['name'], + 'unique_together': {('name', 'website')}, + }, + ), + migrations.CreateModel( + name='Contract', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('title', models.CharField(max_length=31, unique=True)), + ('notice_id', models.CharField(blank=True, max_length=31, null=True)), + ('related_notice_id', models.CharField(blank=True, max_length=31, null=True)), + ('opp_type', models.CharField(blank=True, max_length=63, null=True)), + ('pub_date', models.DateField(blank=True, null=True)), + ('us_dept', models.CharField(blank=True, max_length=31, null=True)), + ('us_dept_sub_tier', models.CharField(blank=True, max_length=31, null=True)), + ('major_dept', models.CharField(blank=True, max_length=31, null=True)), + ('us_office', models.CharField(blank=True, max_length=31, null=True)), + ('award_date', models.CharField(blank=True, max_length=31, null=True)), + ('award_num', models.CharField(blank=True, max_length=31, null=True)), + ('unq_entity_id', models.CharField(blank=True, max_length=31, null=True)), + ('awarded_name', models.CharField(blank=True, max_length=31, null=True)), + ('awarded_addr', models.CharField(blank=True, max_length=31, null=True)), + ('contract_value', models.CharField(blank=True, max_length=31, null=True)), + ('orig_set_aside', models.CharField(blank=True, max_length=127, null=True)), + ('prod_svc_code', models.CharField(blank=True, max_length=127, null=True)), + ('naics_code', models.CharField(blank=True, max_length=127, null=True)), + ('contract_url', models.CharField(blank=True, max_length=127, null=True)), + ('description', models.TextField(blank=True, null=True)), + ('company', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contracts.company')), + ], + options={ + 'verbose_name_plural': 'Contracts', + 'ordering': ['pub_date', 'notice_id'], + 'unique_together': {('notice_id', 'unq_entity_id')}, + }, + ), + migrations.CreateModel( + name='Exec', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=63)), + ('linkedin', models.URLField(blank=True, max_length=127, null=True)), + ('short_desc', models.CharField(blank=True, max_length=63, null=True)), + ('company', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contracts.company')), + ('tags', models.ManyToManyField(blank=True, to='contracts.tags')), + ], + options={ + 'verbose_name_plural': 'Execs', + 'ordering': ['name'], + 'unique_together': {('name', 'company')}, + }, + ), + ] diff --git a/contracts/migrations/0002_contract_pub_date_txt_alter_contract_pub_date.py b/contracts/migrations/0002_contract_pub_date_txt_alter_contract_pub_date.py new file mode 100644 index 0000000..8e161f9 --- /dev/null +++ b/contracts/migrations/0002_contract_pub_date_txt_alter_contract_pub_date.py @@ -0,0 +1,23 @@ +# Generated by Django 6.0.1 on 2026-02-18 19:56 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('contracts', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='contract', + name='pub_date_txt', + field=models.CharField(blank=True, max_length=63, null=True), + ), + migrations.AlterField( + model_name='contract', + name='pub_date', + field=models.DateTimeField(blank=True, null=True), + ), + ] diff --git a/contracts/migrations/0003_alter_contract_company.py b/contracts/migrations/0003_alter_contract_company.py new file mode 100644 index 0000000..e3c9cec --- /dev/null +++ b/contracts/migrations/0003_alter_contract_company.py @@ -0,0 +1,19 @@ +# Generated by Django 6.0.1 on 2026-02-18 20:08 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('contracts', '0002_contract_pub_date_txt_alter_contract_pub_date'), + ] + + operations = [ + migrations.AlterField( + model_name='contract', + name='company', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='contracts.company'), + ), + ] diff --git a/contracts/migrations/0004_alter_contract_notice_id.py b/contracts/migrations/0004_alter_contract_notice_id.py new file mode 100644 index 0000000..db61b21 --- /dev/null +++ b/contracts/migrations/0004_alter_contract_notice_id.py @@ -0,0 +1,19 @@ +# Generated by Django 6.0.1 on 2026-02-18 20:09 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('contracts', '0003_alter_contract_company'), + ] + + operations = [ + migrations.AlterField( + model_name='contract', + name='notice_id', + field=models.CharField(default='000000000000000', max_length=31), + preserve_default=False, + ), + ] diff --git a/contracts/migrations/0005_alter_contract_title.py b/contracts/migrations/0005_alter_contract_title.py new file mode 100644 index 0000000..fb1894e --- /dev/null +++ b/contracts/migrations/0005_alter_contract_title.py @@ -0,0 +1,18 @@ +# Generated by Django 6.0.1 on 2026-02-18 20:12 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('contracts', '0004_alter_contract_notice_id'), + ] + + operations = [ + migrations.AlterField( + model_name='contract', + name='title', + field=models.CharField(max_length=31), + ), + ] diff --git a/contracts/migrations/0006_alter_contract_options_remove_contract_awarded_addr_and_more.py b/contracts/migrations/0006_alter_contract_options_remove_contract_awarded_addr_and_more.py new file mode 100644 index 0000000..9d63385 --- /dev/null +++ b/contracts/migrations/0006_alter_contract_options_remove_contract_awarded_addr_and_more.py @@ -0,0 +1,30 @@ +# Generated by Django 6.0.1 on 2026-02-18 23:04 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('contracts', '0005_alter_contract_title'), + ] + + operations = [ + migrations.AlterModelOptions( + name='contract', + options={'ordering': ['-pub_date', 'notice_id'], 'verbose_name_plural': 'Contracts'}, + ), + migrations.RemoveField( + model_name='contract', + name='awarded_addr', + ), + migrations.RemoveField( + model_name='contract', + name='awarded_name', + ), + migrations.AlterField( + model_name='company', + name='unq_entity_id', + field=models.CharField(blank=True, max_length=63, null=True, unique=True), + ), + ] diff --git a/contracts/migrations/0007_alter_company_unq_entity_id.py b/contracts/migrations/0007_alter_company_unq_entity_id.py new file mode 100644 index 0000000..a395620 --- /dev/null +++ b/contracts/migrations/0007_alter_company_unq_entity_id.py @@ -0,0 +1,18 @@ +# Generated by Django 6.0.1 on 2026-02-18 23:05 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('contracts', '0006_alter_contract_options_remove_contract_awarded_addr_and_more'), + ] + + operations = [ + migrations.AlterField( + model_name='company', + name='unq_entity_id', + field=models.CharField(max_length=63, unique=True), + ), + ] diff --git a/contracts/migrations/0008_alter_contract_award_date.py b/contracts/migrations/0008_alter_contract_award_date.py new file mode 100644 index 0000000..ecd026c --- /dev/null +++ b/contracts/migrations/0008_alter_contract_award_date.py @@ -0,0 +1,18 @@ +# Generated by Django 6.0.1 on 2026-02-18 23:21 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('contracts', '0007_alter_company_unq_entity_id'), + ] + + operations = [ + migrations.AlterField( + model_name='contract', + name='award_date', + field=models.DateField(blank=True, max_length=31, null=True), + ), + ] diff --git a/contracts/migrations/0009_alter_contract_unique_together_alter_contract_title.py b/contracts/migrations/0009_alter_contract_unique_together_alter_contract_title.py new file mode 100644 index 0000000..49183fd --- /dev/null +++ b/contracts/migrations/0009_alter_contract_unique_together_alter_contract_title.py @@ -0,0 +1,22 @@ +# Generated by Django 6.0.1 on 2026-02-18 23:32 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('contracts', '0008_alter_contract_award_date'), + ] + + operations = [ + migrations.AlterUniqueTogether( + name='contract', + unique_together={('notice_id', 'unq_entity_id', 'pub_date_txt')}, + ), + migrations.AlterField( + model_name='contract', + name='title', + field=models.CharField(max_length=254), + ), + ] diff --git a/contracts/migrations/0010_originalcontract_paragraph_alter_contract_award_date_and_more.py b/contracts/migrations/0010_originalcontract_paragraph_alter_contract_award_date_and_more.py new file mode 100644 index 0000000..66c265d --- /dev/null +++ b/contracts/migrations/0010_originalcontract_paragraph_alter_contract_award_date_and_more.py @@ -0,0 +1,45 @@ +# Generated by Django 6.0.1 on 2026-02-19 02:20 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('contracts', '0009_alter_contract_unique_together_alter_contract_title'), + ] + + operations = [ + migrations.CreateModel( + name='OriginalContract', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('number', models.CharField(max_length=31, unique=True)), + ], + ), + migrations.CreateModel( + name='Paragraph', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('date', models.DateField(blank=True, null=True)), + ('link', models.CharField(max_length=255, unique=True)), + ('paragraph', models.TextField(blank=True, null=True)), + ], + ), + migrations.AlterField( + model_name='contract', + name='award_date', + field=models.DateField(blank=True, null=True), + ), + migrations.AddField( + model_name='contract', + name='original_contract_number', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='contracts.originalcontract'), + ), + migrations.AddField( + model_name='originalcontract', + name='para', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contracts.paragraph'), + ), + ] diff --git a/contracts/migrations/0011_alter_paragraph_date_alter_paragraph_link_and_more.py b/contracts/migrations/0011_alter_paragraph_date_alter_paragraph_link_and_more.py new file mode 100644 index 0000000..d8aeba6 --- /dev/null +++ b/contracts/migrations/0011_alter_paragraph_date_alter_paragraph_link_and_more.py @@ -0,0 +1,28 @@ +# Generated by Django 6.0.1 on 2026-02-19 02:33 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('contracts', '0010_originalcontract_paragraph_alter_contract_award_date_and_more'), + ] + + operations = [ + migrations.AlterField( + model_name='paragraph', + name='date', + field=models.DateField(), + ), + migrations.AlterField( + model_name='paragraph', + name='link', + field=models.CharField(max_length=255), + ), + migrations.AlterField( + model_name='paragraph', + name='paragraph', + field=models.TextField(), + ), + ] diff --git a/contracts/migrations/0012_alter_originalcontract_number.py b/contracts/migrations/0012_alter_originalcontract_number.py new file mode 100644 index 0000000..66213fa --- /dev/null +++ b/contracts/migrations/0012_alter_originalcontract_number.py @@ -0,0 +1,18 @@ +# Generated by Django 6.0.1 on 2026-02-19 02:34 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('contracts', '0011_alter_paragraph_date_alter_paragraph_link_and_more'), + ] + + operations = [ + migrations.AlterField( + model_name='originalcontract', + name='number', + field=models.CharField(max_length=63), + ), + ] diff --git a/contracts/migrations/0013_alter_contract_unique_together.py b/contracts/migrations/0013_alter_contract_unique_together.py new file mode 100644 index 0000000..426a8fd --- /dev/null +++ b/contracts/migrations/0013_alter_contract_unique_together.py @@ -0,0 +1,17 @@ +# Generated by Django 6.0.1 on 2026-02-19 02:51 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('contracts', '0012_alter_originalcontract_number'), + ] + + operations = [ + migrations.AlterUniqueTogether( + name='contract', + unique_together=set(), + ), + ] diff --git a/contracts/migrations/0014_alter_paragraph_options.py b/contracts/migrations/0014_alter_paragraph_options.py new file mode 100644 index 0000000..3753cb8 --- /dev/null +++ b/contracts/migrations/0014_alter_paragraph_options.py @@ -0,0 +1,17 @@ +# Generated by Django 6.0.1 on 2026-02-19 08:50 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('contracts', '0013_alter_contract_unique_together'), + ] + + operations = [ + migrations.AlterModelOptions( + name='paragraph', + options={'ordering': ['-date'], 'verbose_name_plural': 'Paragraphs'}, + ), + ] diff --git a/contracts/migrations/__init__.py b/contracts/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/contracts/models.py b/contracts/models.py new file mode 100644 index 0000000..e4b7add --- /dev/null +++ b/contracts/models.py @@ -0,0 +1,139 @@ +from django.db import models +from django.core.files.storage import FileSystemStorage +from django.contrib.auth.models import User + + +class Paragraph(models.Model): + date = models.DateField(blank=False, null=False, unique=False) + link = models.CharField(max_length=255, unique=False) + paragraph = models.TextField(blank=False, null=False) + + + class Meta: + verbose_name_plural = "Paragraphs" + ordering = ['-date'] + + def __unicode__(self): + return "%s" % self.date + + def __str__(self): + return u'%s' % self.date + + +class OriginalContract(models.Model): + number = models.CharField(max_length=63, unique=False) + para = models.ForeignKey(Paragraph, on_delete=models.CASCADE) + + def __unicode__(self): + return "%s" % self.number + + def __str__(self): + return u'%s' % self.number + + +class Tags(models.Model): + name = models.CharField(max_length=31, unique=True) + desc = models.TextField(blank=True, null=True) + + def __unicode__(self): + return "%s" % self.name + + def __str__(self): + return u'%s' % self.name + + +class Tags(models.Model): + name = models.CharField(max_length=31, unique=True) + desc = models.TextField(blank=True, null=True) + + def __unicode__(self): + return "%s" % self.name + + def __str__(self): + return u'%s' % self.name + + + +class Company(models.Model): + name = models.CharField(max_length=63) + unq_entity_id = models.CharField(max_length=63, blank=False, null=False, unique=True) + website = models.URLField(max_length=127, blank=True, null=True) + short_desc = models.CharField(max_length=63, blank=True, null=True) + long_desc = models.TextField(blank=True, null=True) + gmap_link = models.CharField(max_length=253, blank=True, null=True) + tags = models.ManyToManyField(Tags, blank=True) + + address_complete = models.CharField(max_length=127, blank=True, null=True) + address_numbers = models.CharField(max_length=63, blank=True, null=True) + address_type = models.CharField(max_length=31, blank=True, null=True) + city = models.CharField(max_length=127, blank=True, null=True) + state = models.CharField(max_length=127, blank=True, null=True) + zip_code = models.CharField(max_length=15, blank=True, null=True) + + + class Meta: + unique_together = ("name", "website") + verbose_name_plural = "Companies" + ordering = ['name'] + + def __unicode__(self): + return "%s" % self.name + + def __str__(self): + return u'%s' % self.name + + +class Exec(models.Model): + name = models.CharField(max_length=63) + company = models.ForeignKey(Company, on_delete=models.CASCADE) + linkedin = models.URLField(max_length=127, blank=True, null=True) + short_desc = models.CharField(max_length=63, blank=True, null=True) + tags = models.ManyToManyField(Tags, blank=True) + + + class Meta: + unique_together = ("name", "company") + verbose_name_plural = "Execs" + ordering = ['name'] + + def __unicode__(self): + return "%s" % self.name + + def __str__(self): + return u'%s' % self.name + + +class Contract(models.Model): + title = models.CharField(max_length=254, unique=False) + original_contract_number = models.ForeignKey(OriginalContract, on_delete=models.CASCADE, blank=True, null=True) + company = models.ForeignKey(Company, blank=True, null=True, on_delete=models.CASCADE) + notice_id = models.CharField(max_length=31, blank=False, null=False) + related_notice_id = models.CharField(max_length=31, blank=True, null=True) + opp_type = models.CharField(max_length=63, blank=True, null=True) + pub_date = models.DateTimeField(blank=True, null=True) + pub_date_txt = models.CharField(max_length=63, blank=True, null=True) + us_dept = models.CharField(max_length=31, blank=True, null=True) + us_dept_sub_tier = models.CharField(max_length=31, blank=True, null=True) + major_dept = models.CharField(max_length=31, blank=True, null=True) + us_office = models.CharField(max_length=31, blank=True, null=True) + award_date = models.DateField(blank=True, null=True) + award_num = models.CharField(max_length=31, blank=True, null=True) + unq_entity_id = models.CharField(max_length=31, blank=True, null=True) + contract_value = models.CharField(max_length=31, blank=True, null=True) + orig_set_aside = models.CharField(max_length=127, blank=True, null=True) + prod_svc_code = models.CharField(max_length=127, blank=True, null=True) + naics_code = models.CharField(max_length=127, blank=True, null=True) + contract_url = models.CharField(max_length=127, blank=True, null=True) + description = models.TextField(blank=True, null=True) + + + class Meta: + # unique_together = ("notice_id", "unq_entity_id", "pub_date_txt") + verbose_name_plural = "Contracts" + ordering = ['-pub_date', 'notice_id'] + + def __unicode__(self): + return "%s" % self.notice_id + + def __str__(self): + return u'%s' % self.notice_id \ No newline at end of file diff --git a/contracts/serializers.py b/contracts/serializers.py new file mode 100644 index 0000000..0d41802 --- /dev/null +++ b/contracts/serializers.py @@ -0,0 +1,27 @@ +from rest_framework import serializers +from .models import * + +from django.db import models +from django.contrib.auth.models import User +from rest_framework.permissions import BasePermission + +############ +## Events ## +############ + + +class CompanySerializer(serializers.ModelSerializer): + class Meta: + model = Company + # fields = ('id', 'name', 'website', 'city', 'latitude', 'longitude', 'has_map') + fields = '__all__' + + +class ContractSerializer(serializers.ModelSerializer): + company = CompanySerializer(many=False) + # target_language = serializers.SerializerMethodField() + class Meta: + model = Contract + fields = '__all__' + depth = 2 +# fields = ('id', 'name',) \ No newline at end of file diff --git a/contracts/tests.py b/contracts/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/contracts/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/contracts/urls.py b/contracts/urls.py new file mode 100644 index 0000000..af80173 --- /dev/null +++ b/contracts/urls.py @@ -0,0 +1,26 @@ +"""ds_events URL Configuration + +The `urlpatterns` list routes URLs to views. For more information please see: + https://docs.djangoproject.com/en/4.1/topics/http/urls/ +Examples: +Function views + 1. Add an import: from my_app import views + 2. Add a URL to urlpatterns: path('', views.home, name='home') +Class-based views + 1. Add an import: from other_app.views import Home + 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') +Including another URLconf + 1. Import the include() function: from django.urls import include, path + 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) +""" +from django.contrib import admin +from django.urls import path, include, re_path +from .views import * + +urlpatterns = [ + re_path(r'^contracts/', ContractAPIView.as_view(), name="get-contracts-limit"), + re_path(r'^contracts-all/', ContractAllAPIView.as_view(), name="get-contracts"), + re_path(r'^companies/', CompanyAPIView.as_view(), name="get-companies"), + # re_path(r'^events-token/', EventsTokenAPIView.as_view(), name="get-token-events"), + +] diff --git a/contracts/views.py b/contracts/views.py new file mode 100644 index 0000000..60cf381 --- /dev/null +++ b/contracts/views.py @@ -0,0 +1,64 @@ +from django.shortcuts import render +from datetime import datetime, timedelta +import pytz, random + +from .models import * +from .serializers import * + +from django.db.models import Q +from django.db.models import Count + +from rest_framework import generics +from rest_framework.decorators import authentication_classes, permission_classes +from rest_framework.authentication import SessionAuthentication, BasicAuthentication + +from rest_framework.permissions import IsAuthenticated +# from durin.auth import TokenAuthentication + +# from durin.views import APIAccessTokenView + +from django_filters.rest_framework import DjangoFilterBackend +from rest_framework import filters + +from rest_framework.response import Response +from rest_framework_api_key.permissions import HasAPIKey + +td = timedelta(hours=7) +odt = datetime.now() - td + +# Create your views here. + +class ContractAPIView(generics.ListAPIView): + serializer_class = ContractSerializer + queryset = Contract.objects.all()[:25] + filter_backends = [DjangoFilterBackend, filters.SearchFilter] + filterset_fields = ['id',] + permission_classes = [HasAPIKey] + + +class ContractAllAPIView(generics.ListAPIView): + serializer_class = ContractSerializer + queryset = Contract.objects.all() + filter_backends = [DjangoFilterBackend, filters.SearchFilter] + search_fields = ['notice_id', 'original_contract_number', 'title', 'description', 'company__name', 'us_dept', 'us_dept_sub_tier', 'us_office', 'naics_code', 'prod_svc_code'] + filterset_fields = ['id',] + permission_classes = [HasAPIKey] + + +class CompanyAPIView(generics.ListAPIView): + serializer_class = CompanySerializer + queryset = Company.objects.all() + permission_classes = [HasAPIKey] + +# class PromoAPIView(generics.ListAPIView): +# serializer_class = PromoSerializer +# queryset = Promo.objects.filter(published=True) +# filterset_fields = ['organization__name', 'calendar__shortcode',] +# search_fields = ['organization__name', 'calendar__shortcode',] +# # permission_classes = [HasAPIKey] + +# def get_queryset(self): +# calendar = self.request.GET.get('calendar__shortcode') +# queryset = Promo.objects.filter(published=True, calendar__shortcode=calendar).order_by('?') +# return queryset + diff --git a/events/migrations/0049_organization_city_lnk_organization_state_lnk_and_more.py b/events/migrations/0049_organization_city_lnk_organization_state_lnk_and_more.py new file mode 100644 index 0000000..c521a44 --- /dev/null +++ b/events/migrations/0049_organization_city_lnk_organization_state_lnk_and_more.py @@ -0,0 +1,46 @@ +# Generated by Django 6.0.1 on 2026-02-06 18:09 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('events', '0048_organization_has_map'), + ] + + operations = [ + migrations.AddField( + model_name='organization', + name='city_lnk', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='org_city', to='events.place'), + ), + migrations.AddField( + model_name='organization', + name='state_lnk', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='org_state', to='events.place'), + ), + migrations.AlterField( + model_name='organization', + name='org_type', + field=models.CharField(choices=[('Gv', 'Government'), ('Fb', 'Food & Beverage'), ('Re', 'Retail'), ('Se', 'Service'), ('Vn', 'Venue'), ('Ud', 'Undefined')], default='Re', max_length=31), + ), + migrations.AlterField( + model_name='place', + name='connection_type', + field=models.CharField(choices=[('Pc', 'Precinct'), ('Mu', 'Municipality'), ('Ci', 'City'), ('Co', 'County'), ('Ld', 'Legislative District'), ('St', 'State')], default='Ci', max_length=31), + ), + migrations.CreateModel( + name='Official', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=31, unique=True)), + ('website', models.CharField(blank=True, max_length=31, null=True)), + ('position', models.CharField(choices=[('Gv', 'Governor'), ('Sc', 'Secretary'), ('Re', 'Representative'), ('Sn', 'Senator'), ('Sr', 'State Rep'), ('Ss', 'State Senator'), ('Cc', 'County Commissioner'), ('Cm', 'Council Member'), ('Ju', 'Judge'), ('Bm', 'Board Member')], default='Ci', max_length=31)), + ('notes', models.TextField(blank=True, null=True)), + ('boss', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='events.place')), + ('employer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='events.organization')), + ], + ), + ] diff --git a/events/migrations/0050_official_bluesky_official_email_official_instagram_and_more.py b/events/migrations/0050_official_bluesky_official_email_official_instagram_and_more.py new file mode 100644 index 0000000..6ab5f88 --- /dev/null +++ b/events/migrations/0050_official_bluesky_official_email_official_instagram_and_more.py @@ -0,0 +1,38 @@ +# Generated by Django 6.0.1 on 2026-02-18 19:38 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('events', '0049_organization_city_lnk_organization_state_lnk_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='official', + name='bluesky', + field=models.CharField(blank=True, max_length=31, null=True), + ), + migrations.AddField( + model_name='official', + name='email', + field=models.CharField(blank=True, max_length=31, null=True), + ), + migrations.AddField( + model_name='official', + name='instagram', + field=models.CharField(blank=True, max_length=31, null=True), + ), + migrations.AddField( + model_name='official', + name='upscroll', + field=models.CharField(blank=True, max_length=31, null=True), + ), + migrations.AddField( + model_name='official', + name='youtube', + field=models.CharField(blank=True, max_length=31, null=True), + ), + ] diff --git a/events/models.py b/events/models.py index c0bae15..62ddd10 100644 --- a/events/models.py +++ b/events/models.py @@ -80,7 +80,6 @@ class Place(models.Model): return u'%s' % self.name - class Official(models.Model): POSITION_TYPE = ( ('Gv', 'Governor'), @@ -96,12 +95,18 @@ class Official(models.Model): ) name = models.CharField(max_length=31, unique=True) - website = models.CharField(max_length=31,blank=True, null=True) boss = models.ForeignKey("Place", on_delete=models.CASCADE) employer = models.ForeignKey("Organization", on_delete=models.CASCADE) position = models.CharField(max_length=31, choices=POSITION_TYPE, default='Ci') notes = models.TextField(blank=True, null=True) + website = models.CharField(max_length=31, blank=True, null=True) + email = models.CharField(max_length=31, blank=True, null=True) + bluesky = models.CharField(max_length=31, blank=True, null=True) + instagram = models.CharField(max_length=31, blank=True, null=True) + youtube = models.CharField(max_length=31, blank=True, null=True) + upscroll = models.CharField(max_length=31, blank=True, null=True) + def __unicode__(self): return "%s" % self.name @@ -135,8 +140,8 @@ class Organization(models.Model): barrio = models.CharField(max_length=127, blank=True, null=True) city = models.CharField(max_length=127, blank=True, null=True) state = models.CharField(max_length=127, blank=True, null=True) - city = models.ForeignKey(Place, on_delete=models.CASCADE, related_name="org_city") - state = models.ForeignKey(Place, on_delete=models.CASCADE, related_name="org_state" ) + city_lnk = models.ForeignKey(Place, on_delete=models.CASCADE, blank=True, null=True, related_name="org_city") + state_lnk = models.ForeignKey(Place, on_delete=models.CASCADE, blank=True, null=True, related_name="org_state" ) zip_code = models.CharField(max_length=15, blank=True, null=True) phone_number = models.CharField(max_length=255, blank=True, null=True) diff --git a/media/promo/AtlasObscura.png b/media/promo/AtlasObscura.png new file mode 100644 index 0000000..34139ba Binary files /dev/null and b/media/promo/AtlasObscura.png differ