Skip to content

Commit

Permalink
Calculate credit in real time
Browse files Browse the repository at this point in the history
  • Loading branch information
leduythuccs committed Sep 19, 2024
1 parent 57ea4b0 commit 37b1d4f
Show file tree
Hide file tree
Showing 11 changed files with 402 additions and 176 deletions.
7 changes: 7 additions & 0 deletions dmoj/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,13 @@
# List of subdomain that will be ignored in organization subdomain middleware
VNOJ_IGNORED_ORGANIZATION_SUBDOMAINS = ['oj', 'www', 'localhost']

# Enable organization credit system, if true, org will not be able to submit submissions
# if they run out of credit
VNOJ_ENABLE_ORGANIZATION_CREDIT_LIMITATION = False
# 3 hours free per month
VNOJ_MONTHLY_FREE_CREDIT = 3 * 60 * 60
VNOJ_PRICE_PER_HOUR = 50

# Some problems have a lot of testcases, and each testcase
# has about 5~6 fields, so we need to raise this
DATA_UPLOAD_MAX_NUMBER_FIELDS = 3000
Expand Down
6 changes: 3 additions & 3 deletions judge/admin/organization.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,9 @@ class Meta:


class OrganizationAdmin(VersionAdmin):
readonly_fields = ('creation_date',)
fields = ('name', 'slug', 'short_name', 'is_open', 'is_unlisted', 'about', 'logo_override_image', 'slots',
'creation_date', 'admins')
readonly_fields = ('creation_date', 'current_consumed_credit')
fields = ('name', 'slug', 'short_name', 'is_open', 'is_unlisted', 'available_credit', 'current_consumed_credit',
'about', 'logo_override_image', 'slots', 'creation_date', 'admins')
list_display = ('name', 'short_name', 'is_open', 'is_unlisted', 'slots', 'show_public')
prepopulated_fields = {'slug': ('name',)}
actions = ('recalculate_points',)
Expand Down
1 change: 1 addition & 0 deletions judge/bridge/judge_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -424,6 +424,7 @@ def on_grading_end(self, packet):
problem._updating_stats_only = True
problem.update_stats()
submission.update_contest()
submission.update_credit(total_time)

finished_submission(submission)

Expand Down
50 changes: 50 additions & 0 deletions judge/management/commands/backfill_current_credit.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
from django.conf import settings
from django.core.management.base import BaseCommand
from django.db.models import ExpressionWrapper, FloatField, Sum
from django.utils import timezone

from judge.models import Organization, Submission


class Command(BaseCommand):
help = 'backfill current credit usage for all organizations'

def backfill_current_credit(self, org: Organization, month_start):
credit_problem = (
Submission.objects.filter(
problem__organizations=org,
contest_object__isnull=True,
date__gte=month_start,
)
.annotate(
credit=ExpressionWrapper(
Sum('test_cases__time'), output_field=FloatField(),
),
)
.aggregate(Sum('credit'))['credit__sum'] or 0
)

credit_contest = (
Submission.objects.filter(
contest_object__organizations=org,
date__gte=month_start,
)
.annotate(
credit=ExpressionWrapper(
Sum('test_cases__time'), output_field=FloatField(),
),
)
.aggregate(Sum('credit'))['credit__sum'] or 0
)

org.monthly_credit = settings.VNOJ_MONTHLY_FREE_CREDIT

org.consume_credit(credit_problem + credit_contest)

def handle(self, *args, **options):
# get current month
start = timezone.now().replace(day=1, hour=0, minute=0, second=0, microsecond=0)
print('Processing', start, 'at time', timezone.now())

for org in Organization.objects.all():
self.backfill_current_credit(org, start)
28 changes: 28 additions & 0 deletions judge/migrations/0207_org_credit.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Generated by Django 3.2.19 on 2024-09-19 02:02

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('judge', '0206_monthly_credit'),
]

operations = [
migrations.AddField(
model_name='organization',
name='available_credit',
field=models.FloatField(default=0, help_text='Available credits'),
),
migrations.AddField(
model_name='organization',
name='current_consumed_credit',
field=models.FloatField(default=0, help_text='Total used credit this month'),
),
migrations.AddField(
model_name='organization',
name='monthly_credit',
field=models.FloatField(default=0, help_text='Total monthly free credit left'),
),
]
20 changes: 20 additions & 0 deletions judge/models/profile.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,9 @@ class Organization(models.Model):
'viewing the organization.'))
performance_points = models.FloatField(default=0)
member_count = models.IntegerField(default=0)
current_consumed_credit = models.FloatField(default=0, help_text='Total used credit this month')
available_credit = models.FloatField(default=0, help_text='Available credits')
monthly_credit = models.FloatField(default=0, help_text='Total monthly free credit left')

_pp_table = [pow(settings.VNOJ_ORG_PP_STEP, i) for i in range(settings.VNOJ_ORG_PP_ENTRIES)]

Expand Down Expand Up @@ -110,6 +113,23 @@ def get_absolute_url(self):
def get_users_url(self):
return reverse('organization_users', args=[self.slug])

def has_credit_left(self):
return self.current_consumed_credit < self.available_credit + self.monthly_credit

def consume_credit(self, consumed):
# reduce credit in monthly credit first
# then reduce the left to available credit
if self.monthly_credit >= consumed:
self.monthly_credit -= consumed
else:
consumed -= self.monthly_credit
self.monthly_credit = 0
# if available credit can be negative if we don't enable the monthly credit limitation
self.available_credit -= consumed

self.current_consumed_credit += consumed
self.save(update_fields=['monthly_credit', 'available_credit', 'current_consumed_credit'])

class Meta:
ordering = ['name']
permissions = (
Expand Down
21 changes: 21 additions & 0 deletions judge/models/submission.py
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,27 @@ def update_contest(self):

update_contest.alters_data = True

def update_credit(self, consumed_credit):
problem = self.problem

organizations = []
if problem.is_organization_private:
organizations = problem.organizations.all()

if len(organizations) == 0:
try:
contest_object = self.contest_object
except AttributeError:
pass

if contest_object.is_organization_private:
organizations = contest_object.organizations.all()

for organization in organizations:
organization.consume_credit(consumed_credit)

update_credit.alters_data = True

@property
def is_graded(self):
return self.status not in ('QU', 'P', 'G')
Expand Down
29 changes: 26 additions & 3 deletions judge/views/organization.py
Original file line number Diff line number Diff line change
Expand Up @@ -581,16 +581,39 @@ def get_context_data(self, **kwargs):
context['title'] = self.organization.name

usages = context['usages']
days = [usage['time'].isoformat() for usage in usages]
days = [usage['time'].isoformat() for usage in usages] + [_('Current month')]
used_credits = [usage['consumed_credit'] for usage in usages] + [self.organization.current_consumed_credit]
sec_per_hour = 60 * 60
chart = get_lines_chart(days, {
_('Credit usage (hour)'): [round(usage['consumed_credit'] / 60 / 60, 2) for usage in usages],
_('Credit usage (hour)'): [
round(credit / sec_per_hour, 2) for credit in used_credits
],
})

cost_chart = get_lines_chart(days, {
_('Cost (thousand vnd)'): [
round(max(0, usage['consumed_credit'] / 60 / 60 - 3) * 50, 3) for usage in usages
round(
max(0, credit - settings.VNOJ_MONTHLY_FREE_CREDIT) / sec_per_hour * settings.VNOJ_PRICE_PER_HOUR, 3,
) for credit in used_credits
],
})

monthly_credit = int(self.organization.monthly_credit)

context['monthly_credit'] = {
'hour': monthly_credit // sec_per_hour,
'minute': (monthly_credit % sec_per_hour) // 60,
'second': monthly_credit % 60,
}

available_credit = int(self.organization.available_credit)

context['available_credit'] = {
'hour': available_credit // sec_per_hour,
'minute': (available_credit % sec_per_hour) // 60,
'second': available_credit % 60,
}

context['credit_chart'] = chart
context['cost_chart'] = cost_chart
return context
Expand Down
28 changes: 28 additions & 0 deletions judge/views/problem.py
Original file line number Diff line number Diff line change
Expand Up @@ -652,6 +652,34 @@ def form_valid(self, form):
return generic_message(self.request, _('Too many submissions'),
_('You have exceeded the submission limit for this problem.'))

if settings.VNOJ_ENABLE_ORGANIZATION_CREDIT_LIMITATION:
# check if the problem belongs to any organization
organizations = []
if self.object.is_organization_private:
organizations = self.object.organizations.all()

if len(organizations) == 0:
# check if the contest belongs to any organization
if self.contest_problem is not None:
contest_object = self.request.profile.current_contest.contest

if contest_object.is_organization_private:
organizations = contest_object.organizations.all()

# check if org have credit to execute this submission
for org in organizations:
if not org.has_credit_left():
org_name = org.name
return generic_message(
self.request,
_('No credit'),
_(
'The organization %s has no credit left to execute this submission. '
'Ask the organization to buy more credit.',
)
% org_name,
)

with transaction.atomic():
self.new_submission = form.save(commit=False)

Expand Down
Loading

0 comments on commit 37b1d4f

Please sign in to comment.