Skip to content

Commit

Permalink
Merge pull request #83 from eldersantoss/multi-service-deploy
Browse files Browse the repository at this point in the history
Multi service deploy
  • Loading branch information
eldersantoss authored Jun 21, 2024
2 parents 74f5f9b + 0149d13 commit b754434
Show file tree
Hide file tree
Showing 11 changed files with 235 additions and 130 deletions.
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -137,3 +137,8 @@ dmypy.json

# ssh keys
*.pem


# Database backups

dumps/*
1 change: 1 addition & 0 deletions core/constants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
SEGUNDOS_24_HORAS = 60 * 60 * 24
21 changes: 13 additions & 8 deletions core/management/commands/create_and_update_matches.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
from django.core.cache import cache
from django.core.management.base import BaseCommand, CommandParser

from core.models import Competition


class Command(BaseCommand):
help = "For each registered competition, send a request to the Football API to create new matches and update those already registered."
help = "For each registered competition, uses the Football API to create new matches and update those already registered."

def add_arguments(self, parser: CommandParser) -> None:
parser.add_argument(
Expand All @@ -25,13 +26,17 @@ def handle(self, *args, **options):
competitions = Competition.objects.all()
if not competitions.exists():
self.stdout.write("No competitions registered yet.")
return

for competition in competitions:
created_matches, updated_matches = competition.create_and_update_matches(
days_from, days_ahead
)
self.stdout.write(
f"{len(created_matches)} matches created and {len(updated_matches)} updated matches for {competition}..."
)
for comp in competitions:
try:
created, updated = comp.create_and_update_matches(days_from, days_ahead)
self.stdout.write(f"{len(created)} matches created and {len(updated)} updated matches for {comp}...")

except Exception as e:
self.stderr.write(f"Error when updating {comp}: {e}")

self.stdout.write("Cleaning cache data...")
cache.clear()

self.stdout.write("Competitions update done.")
90 changes: 20 additions & 70 deletions core/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,7 @@ def __str__(self) -> str:
return self.name

def logo_url(self):
return (
f"https://media.api-sports.io/football/teams/{self.data_source_id}.png"
if self.data_source_id
else ""
)
return f"https://media.api-sports.io/football/teams/{self.data_source_id}.png" if self.data_source_id else ""


class Competition(models.Model):
Expand Down Expand Up @@ -75,11 +71,7 @@ def get_teams(self):
params = {"league": self.real_data_source_id, "season": self.season}

# TODO: tratar requests.exceptions.ConnectionError:
response = (
requests.get(source_url, headers=headers, params=params)
.json()
.get("response")
)
response = requests.get(source_url, headers=headers, params=params).json().get("response")
sleep(settings.FOOTBALL_API_RATE_LIMIT_TIME)

teams = []
Expand Down Expand Up @@ -114,9 +106,7 @@ def create_and_update_matches(self, days_from: int | None, days_ahead: int | Non
"season": self.season,
"from": str(from_),
"to": str(to),
"status": "-".join(
[Match.NOT_STARTED, *Match.IN_PROGRESS_AND_FINISHED_STATUS]
),
"status": "-".join([Match.NOT_STARTED, *Match.IN_PROGRESS_AND_FINISHED_STATUS]),
}

# TODO: tratar requests.exceptions.ConnectionError:
Expand Down Expand Up @@ -245,9 +235,7 @@ def update_matches(self, days_from: int | None, days_ahead: int | None):
away_goals = data["goals"]["away"]

match = (
Match.objects.exclude(status__in=Match.FINISHED_STATUS)
.filter(data_source_id=data_source_id)
.first()
Match.objects.exclude(status__in=Match.FINISHED_STATUS).filter(data_source_id=data_source_id).first()
)

if match is None:
Expand All @@ -266,9 +254,7 @@ def get_with_matches_on_period(cls, from_: date, to: date):
"""Returns competitions which that have matches on period"""

matches_on_period = Match.get_happen_on_period(from_, to)
competitions_with_matches_on_period = matches_on_period.values(
"competition"
).distinct()
competitions_with_matches_on_period = matches_on_period.values("competition").distinct()

return cls.objects.filter(id__in=competitions_with_matches_on_period)

Expand Down Expand Up @@ -371,18 +357,12 @@ def update_status(
):
self.status = new_status

match_time_limit = self.date_time + timezone.timedelta(
minutes=match_time_limit_minutes
)
match_time_limit = self.date_time + timezone.timedelta(minutes=match_time_limit_minutes)
match_broke_limit_time = timezone.now() >= match_time_limit

REGULAR_MATCH_DURATION = 90

if (
match_broke_limit_time
and elapsed_time >= REGULAR_MATCH_DURATION
and self.status == Match.SECOND_HALF
):
if match_broke_limit_time and elapsed_time >= REGULAR_MATCH_DURATION and self.status == Match.SECOND_HALF:
self.status = Match.FINISHED_STATUS

def is_finished(self) -> bool:
Expand Down Expand Up @@ -412,14 +392,8 @@ def result_str(self):
description="Aberta para palpites?",
)
def open_to_guesses(self):
return (
self.date_time
> timezone.now()
+ timezone.timedelta(minutes=self.MINUTES_BEFORE_START_MATCH)
) and (
self.date_time
<= timezone.now()
+ timezone.timedelta(hours=self.HOURS_BEFORE_OPEN_TO_GUESSES)
return (self.date_time > timezone.now() + timezone.timedelta(minutes=self.MINUTES_BEFORE_START_MATCH)) and (
self.date_time <= timezone.now() + timezone.timedelta(hours=self.HOURS_BEFORE_OPEN_TO_GUESSES)
)

def get_pools(self):
Expand Down Expand Up @@ -475,11 +449,7 @@ def get_involved_pools(self):
def get_who_should_be_notified_by_email(cls):
"""Returns guessers that should be notified by email"""

return (
cls.objects.exclude(user__email="")
.exclude(pools__isnull=True)
.exclude(receive_notifications=False)
)
return cls.objects.exclude(user__email="").exclude(pools__isnull=True).exclude(receive_notifications=False)

def get_involved_pools_with_new_matches(self):
"""Returns pools with new matches that this guesser is involved
Expand Down Expand Up @@ -529,9 +499,7 @@ class Meta:

def __str__(self) -> str:
return (
f"{self.match.home_team.name} {self.home_goals}"
+ " x "
+ f"{self.away_goals} {self.match.away_team.name}"
f"{self.match.home_team.name} {self.home_goals}" + " x " + f"{self.away_goals} {self.match.away_team.name}"
)

def get_score(self) -> int:
Expand Down Expand Up @@ -563,16 +531,10 @@ def _evaluate(self):
ACERTO_VISITANTE_VENCEDOR = (self.home_goals < self.away_goals) and (
self.match.home_goals < self.match.away_goals
)
ACERTO_EMPATE = (self.home_goals == self.away_goals) and (
self.match.home_goals == self.match.away_goals
)
ACERTO_EMPATE = (self.home_goals == self.away_goals) and (self.match.home_goals == self.match.away_goals)
ACERTO_PARCIAL = ACERTO_MANDANTE_VENCEDOR or ACERTO_VISITANTE_VENCEDOR
ACERTO_PARCIAL_COM_GOLS = ACERTO_PARCIAL and (
ACERTO_DE_GOLS_MANDANTE or ACERTO_DE_GOLS_VISITANTE
)
ACERTO_SOMENTE_GOLS = (
ACERTO_DE_GOLS_MANDANTE or ACERTO_DE_GOLS_VISITANTE
) and not ACERTO_PARCIAL
ACERTO_PARCIAL_COM_GOLS = ACERTO_PARCIAL and (ACERTO_DE_GOLS_MANDANTE or ACERTO_DE_GOLS_VISITANTE)
ACERTO_SOMENTE_GOLS = (ACERTO_DE_GOLS_MANDANTE or ACERTO_DE_GOLS_VISITANTE) and not ACERTO_PARCIAL
ACERTO_CRAVADO = ACERTO_DE_GOLS_MANDANTE and ACERTO_DE_GOLS_VISITANTE

PONTUACAO_ACERTO_CRAVADO = 10
Expand Down Expand Up @@ -705,10 +667,8 @@ def get_open_matches(self):
"""Returns matches open to guesses"""

return self.get_matches().filter(
date_time__gt=timezone.now()
+ timezone.timedelta(minutes=self.minutes_before_start_match),
date_time__lte=timezone.now()
+ timezone.timedelta(hours=self.hours_before_open_to_guesses),
date_time__gt=timezone.now() + timezone.timedelta(minutes=self.minutes_before_start_match),
date_time__lte=timezone.now() + timezone.timedelta(hours=self.hours_before_open_to_guesses),
)

@classmethod
Expand Down Expand Up @@ -738,9 +698,7 @@ def add_guess_to_pools(self, guess: Guess, for_all_pools: bool):
if for_all_pools:
pools_with_the_match = match.get_pools()
pools_with_the_guesser = guesser.pools.all()
pools_with_match_and_guesser = pools_with_the_match.intersection(
pools_with_the_guesser
)
pools_with_match_and_guesser = pools_with_the_match.intersection(pools_with_the_guesser)

for pool in pools_with_match_and_guesser:
self._replace_guess_in_pool(guess, pool)
Expand Down Expand Up @@ -804,9 +762,7 @@ def get_guessers_with_score_and_guesses(
guessers = self.get_guessers_with_match_scores(matches)

for guesser in guessers:
guesser.matches_and_guesses = self._get_guesses_per_matches(
guesser, matches
)
guesser.matches_and_guesses = self._get_guesses_per_matches(guesser, matches)

return guessers

Expand Down Expand Up @@ -838,11 +794,7 @@ def _assemble_datetime_period(self, month: int, year: int, week: int):
else:
# período semanal (ano, mes e semanas recebidos)
start = timezone.now().fromisocalendar(year, week, 1)
end = (
timezone.now()
.fromisocalendar(year, week, 7)
.replace(hour=23, minute=59, second=59)
)
end = timezone.now().fromisocalendar(year, week, 7).replace(hour=23, minute=59, second=59)

return start, end

Expand All @@ -862,9 +814,7 @@ def get_finished_or_in_progress_matches_on_period(
)

def get_guessers_with_match_scores(self, matches: Iterable[Match]):
guesses_of_this_pool_in_the_period = Q(guesses__match__in=matches) & Q(
guesses__in=self.guesses.all()
)
guesses_of_this_pool_in_the_period = Q(guesses__match__in=matches) & Q(guesses__in=self.guesses.all())
sum_expr = Sum(
"guesses__score",
filter=guesses_of_this_pool_in_the_period,
Expand Down
24 changes: 8 additions & 16 deletions core/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,14 @@
from django.forms import CheckboxSelectMultiple, modelform_factory
from django.shortcuts import get_object_or_404, render
from django.utils import timezone
from django.utils.decorators import method_decorator
from django.utils.text import slugify
from django.views import generic
from django.views.decorators.cache import cache_page

from core.helpers import redirect_with_msg

from . import constants
from .forms import GuesserEditForm, GuessForm, RankingPeriodForm, UserEditForm
from .models import Guess, GuessPool
from .viewmixins import GuessPoolMembershipMixin
Expand All @@ -23,11 +26,7 @@ def get_context_data(self, **kwargs):
involved_pools = guesser.get_involved_pools()
pools_as_guesser = guesser.pools.all()
for pool in involved_pools:
pool.is_pending = (
pool.has_pending_match(self.request.user.guesser)
if pool in pools_as_guesser
else False
)
pool.is_pending = pool.has_pending_match(self.request.user.guesser) if pool in pools_as_guesser else False

context["display_subtitle"] = any([pool.is_pending for pool in involved_pools])
context["pools"] = involved_pools
Expand Down Expand Up @@ -234,10 +233,7 @@ def get(self, request, *args, **kwargs):
return redirect_with_msg(
self.request,
"error",
"Nenhum bolão público cadastrado..."
+ " Que tal criar um agora mesmo?"
+ " Basta clicar em <strong>Criar bolão</strong>"
+ " e configurar como quiser 😎",
"Nenhum bolão público cadastrado... Que tal criar um agora mesmo? Basta clicar em <strong>Criar bolão</strong> e configurar como quiser 😎",
"long",
)
return super().get(request, *args, **kwargs)
Expand All @@ -253,10 +249,7 @@ def dispatch(self, request, *args, **kwargs):
return redirect_with_msg(
self.request,
"error",
"Você não está cadastrado como palpiteiro."
+ " Acesse <strong>Gerenciar bolão</strong>"
+ " e marque seu usuário como <strong>Palpiteiro</strong>"
+ " para ter acesso à esta ação.",
"Você não está cadastrado como palpiteiro. Acesse <strong>Gerenciar bolão</strong> e marque seu usuário como <strong>Palpiteiro</strong> para ter acesso à esta ação.",
"long",
self.pool,
)
Expand Down Expand Up @@ -372,6 +365,7 @@ def post(self, *args, **kwargs):
class RankingView(LoginRequiredMixin, GuessPoolMembershipMixin, generic.TemplateView):
template_name = "core/ranking.html"

@method_decorator(cache_page(constants.SEGUNDOS_24_HORAS))
def get(self, *args, **kwargs):
context = self.get_context_data(**kwargs)
if not context["guessers"].exists():
Expand Down Expand Up @@ -400,8 +394,6 @@ def get_context_data(self, **kwargs):
week = int(form.cleaned_data["semana"])

context["period_form"] = form
context["guessers"] = self.pool.get_guessers_with_score_and_guesses(
month, year, week
)
context["guessers"] = self.pool.get_guessers_with_score_and_guesses(month, year, week)

return context
21 changes: 16 additions & 5 deletions palpiteiros/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,8 +125,8 @@
LANGUAGE_CODE = "pt-br"
USE_I18N = True

TIME_ZONE = config("TIME_ZONE")
USE_TZ = config("USE_TZ", cast=bool)
TIME_ZONE = config("TIME_ZONE", default="America/Sao_Paulo")
USE_TZ = config("USE_TZ", default=1, cast=bool)


# Static files (CSS, JavaScript, Images)
Expand Down Expand Up @@ -154,9 +154,7 @@

# Email

EMAIL_BACKEND = config(
"EMAIL_BACKEND", default="django.core.mail.backends.smtp.EmailBackend"
)
EMAIL_BACKEND = config("EMAIL_BACKEND", default="django.core.mail.backends.smtp.EmailBackend")
EMAIL_HOST = config("EMAIL_HOST", default="smtp.gmail.com")
EMAIL_HOST_USER = config("EMAIL_HOST_USER")
EMAIL_HOST_PASSWORD = config("EMAIL_HOST_PASSWORD")
Expand Down Expand Up @@ -191,3 +189,16 @@
FOOTBALL_API_KEY = config("FOOTBALL_API_KEY")
FOOTBALL_API_HOST = "v3.football.api-sports.io"
FOOTBALL_API_RATE_LIMIT_TIME = 7


# Cache

CACHES = {
"default": {
"BACKEND": "django_redis.cache.RedisCache",
"LOCATION": config("CACHE_LOCATION"),
"OPTIONS": {
"CLIENT_CLASS": "django_redis.client.DefaultClient",
},
}
}
36 changes: 36 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
[tool.black]
line-length = 120

[tool.isort]
profile = "black"

[tool.flake8]
max-line-length = 120
max-complexity = 7
exclude = ["migrations", "venv"]
ignore = ["E501"]

[tool.pytest.ini_options]
python_files = ["test_*.py", "tests.py"]
norecursedirs = ["venv", "old_tests"]
addopts = """
--ds=botscontabeis.settings --no-migrations --reuse-db
--pdbcls=IPython.terminal.debugger:TerminalPdb
--color=yes --quiet --numprocesses=auto
--cov --cov-report=html --no-cov-on-fail --maxfail=1 --cov-fail-under=80 --cov-branch
"""

[tool.coverage.run]
branch = true
source = ["."]
omit = [
"*/tests/*",
"*/migrations/*",
"venv/*",
"static/*",
"manage.py",
"requirements.txt",
]

[tool.ipdb]
context=5
Loading

0 comments on commit b754434

Please sign in to comment.