| @ -1,3 +0,0 @@ | |||||
| # Fuente usada | |||||
| https://docs.djangoproject.com/en/5.0/intro/tutorial01/ | |||||
| @ -1,22 +0,0 @@ | |||||
| from django.contrib import admin | |||||
| from .models import Choice, Question | |||||
| class ChoiceInline(admin.TabularInline): | |||||
| model = Choice | |||||
| extra = 3 | |||||
| class QuestionAdmin(admin.ModelAdmin): | |||||
| fieldsets = [ | |||||
| (None, {"fields": ["question_text"]}), | |||||
| ("Date information", {"fields": ["pub_date"], "classes": ["collapse"]}), | |||||
| ] | |||||
| inlines = [ChoiceInline] | |||||
| list_display = ["question_text", "pub_date", "was_published_recently"] | |||||
| list_filter = ["pub_date"] | |||||
| search_fields = ["question_text"] | |||||
| admin.site.register(Question, QuestionAdmin) | |||||
| @ -1,32 +0,0 @@ | |||||
| # Generated by Django 5.0.7 on 2024-07-18 08:36 | |||||
| import django.db.models.deletion | |||||
| from django.db import migrations, models | |||||
| class Migration(migrations.Migration): | |||||
| initial = True | |||||
| dependencies = [ | |||||
| ] | |||||
| operations = [ | |||||
| migrations.CreateModel( | |||||
| name='Question', | |||||
| fields=[ | |||||
| ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), | |||||
| ('question_text', models.CharField(max_length=200)), | |||||
| ('pub_date', models.DateTimeField(verbose_name='date published')), | |||||
| ], | |||||
| ), | |||||
| migrations.CreateModel( | |||||
| name='Choice', | |||||
| fields=[ | |||||
| ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), | |||||
| ('choice_text', models.CharField(max_length=200)), | |||||
| ('votes', models.IntegerField(default=0)), | |||||
| ('question', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='polls.question')), | |||||
| ], | |||||
| ), | |||||
| ] | |||||
| @ -1,34 +0,0 @@ | |||||
| import datetime | |||||
| from django.db import models | |||||
| from django.utils import timezone | |||||
| from django.contrib import admin | |||||
| # Create your models here. | |||||
| class Question(models.Model): | |||||
| question_text = models.CharField(max_length=200) | |||||
| pub_date = models.DateTimeField("date published") | |||||
| def __str__(self): | |||||
| return self.question_text | |||||
| @admin.display( | |||||
| boolean=True, | |||||
| ordering="pub_date", | |||||
| description="Published recently?", | |||||
| ) | |||||
| def was_published_recently(self): | |||||
| now = timezone.now() | |||||
| return now - datetime.timedelta(days=1) <= self.pub_date <= now | |||||
| class Choice(models.Model): | |||||
| question = models.ForeignKey(Question, on_delete=models.CASCADE) | |||||
| choice_text = models.CharField(max_length=200) | |||||
| votes = models.IntegerField(default=0) | |||||
| def __str__(self): | |||||
| return self.choice_text | |||||
| @ -1,8 +0,0 @@ | |||||
| li a { | |||||
| color: green; | |||||
| } | |||||
| body { | |||||
| background: white url("images/background.jpeg") no-repeat; | |||||
| background-size: 50% 50%; | |||||
| } | |||||
| @ -1,12 +0,0 @@ | |||||
| <form action="{% url 'polls:vote' question.id %}" method="post"> | |||||
| {% csrf_token %} | |||||
| <fieldset> | |||||
| <legend><h1>{{ question.question_text }}</h1></legend> | |||||
| {% if error_message %}<p><strong>{{ error_message }}</strong></p>{% endif %} | |||||
| {% for choice in question.choice_set.all %} | |||||
| <input type="radio" name="choice" id="choice{{ forloop.counter }}" value="{{ choice.id }}"> | |||||
| <label for="choice{{ forloop.counter }}">{{ choice.choice_text }}</label><br> | |||||
| {% endfor %} | |||||
| </fieldset> | |||||
| <input type="submit" value="Vote"> | |||||
| </form> | |||||
| @ -1,13 +0,0 @@ | |||||
| {% load static %} | |||||
| <link rel="stylesheet" href="{% static 'polls/style.css' %}"> | |||||
| {% if latest_question_list %} | |||||
| <ul> | |||||
| {% for question in latest_question_list %} | |||||
| <li><a href="{% url 'polls:detail' question.id %}">{{ question.question_text }}</a></li> | |||||
| {% endfor %} | |||||
| </ul> | |||||
| {% else %} | |||||
| <p>No polls are available.</p> | |||||
| {% endif %} | |||||
| @ -1,9 +0,0 @@ | |||||
| <h1>{{ question.question_text }}</h1> | |||||
| <ul> | |||||
| {% for choice in question.choice_set.all %} | |||||
| <li>{{ choice.choice_text }} -- {{ choice.votes }} vote{{ choice.votes|pluralize }}</li> | |||||
| {% endfor %} | |||||
| </ul> | |||||
| <a href="{% url 'polls:detail' question.id %}">Vote again?</a> | |||||
| @ -1,102 +0,0 @@ | |||||
| import datetime | |||||
| from django.test import TestCase | |||||
| from django.utils import timezone | |||||
| from django.urls import reverse | |||||
| from .models import Question | |||||
| def create_question(question_text, days): | |||||
| """ | |||||
| Create a question with the given `question_text` and published the | |||||
| given number of `days` offset to now (negative for questions published | |||||
| in the past, positive for questions that have yet to be published). | |||||
| """ | |||||
| time = timezone.now() + datetime.timedelta(days=days) | |||||
| return Question.objects.create(question_text=question_text, pub_date=time) | |||||
| class QuestionModelTests(TestCase): | |||||
| def test_was_published_recently_with_future_question(self): | |||||
| """ | |||||
| was_published_recently() returns False for questions whose pub_date | |||||
| is in the future. | |||||
| """ | |||||
| time = timezone.now() + datetime.timedelta(days=30) | |||||
| future_question = Question(pub_date=time) | |||||
| self.assertIs(future_question.was_published_recently(), False) | |||||
| def test_was_published_recently_with_old_question(self): | |||||
| """ | |||||
| was_published_recently() returns False for questions whose pub_date | |||||
| is older than 1 day. | |||||
| """ | |||||
| time = timezone.now() - datetime.timedelta(days=1, seconds=1) | |||||
| old_question = Question(pub_date=time) | |||||
| self.assertIs(old_question.was_published_recently(), False) | |||||
| def test_was_published_recently_with_recent_question(self): | |||||
| """ | |||||
| was_published_recently() returns True for questions whose pub_date | |||||
| is within the last day. | |||||
| """ | |||||
| time = timezone.now() - datetime.timedelta(hours=23, minutes=59, seconds=59) | |||||
| recent_question = Question(pub_date=time) | |||||
| self.assertIs(recent_question.was_published_recently(), True) | |||||
| class QuestionIndexViewTests(TestCase): | |||||
| def test_no_questions(self): | |||||
| """ | |||||
| If no questions exist, an appropriate message is displayed. | |||||
| """ | |||||
| response = self.client.get(reverse("polls:index")) | |||||
| self.assertEqual(response.status_code, 200) | |||||
| self.assertContains(response, "No polls are available.") | |||||
| self.assertQuerySetEqual(response.context["latest_question_list"], []) | |||||
| def test_future_question(self): | |||||
| """ | |||||
| The detail view of a question with a pub_date in the future | |||||
| returns a 404 not found. | |||||
| """ | |||||
| future_question = create_question(question_text="Future question.", days=5) | |||||
| url = reverse("polls:detail", args=(future_question.id,)) | |||||
| response = self.client.get(url) | |||||
| self.assertEqual(response.status_code, 404) | |||||
| def test_past_question(self): | |||||
| """ | |||||
| The detail view of a question with a pub_date in the past | |||||
| displays the question's text. | |||||
| """ | |||||
| past_question = create_question(question_text="Past Question.", days=-5) | |||||
| url = reverse("polls:detail", args=(past_question.id,)) | |||||
| response = self.client.get(url) | |||||
| self.assertContains(response, past_question.question_text) | |||||
| def test_future_question_and_past_question(self): | |||||
| """ | |||||
| Even if both past and future questions exist, only past questions | |||||
| are displayed. | |||||
| """ | |||||
| question = create_question(question_text="Past question.", days=-30) | |||||
| create_question(question_text="Future question.", days=30) | |||||
| response = self.client.get(reverse("polls:index")) | |||||
| self.assertQuerySetEqual( | |||||
| response.context["latest_question_list"], | |||||
| [question], | |||||
| ) | |||||
| def test_two_past_questions(self): | |||||
| """ | |||||
| The questions index page may display multiple questions. | |||||
| """ | |||||
| question1 = create_question(question_text="Past question 1.", days=-30) | |||||
| question2 = create_question(question_text="Past question 2.", days=-5) | |||||
| response = self.client.get(reverse("polls:index")) | |||||
| self.assertQuerySetEqual( | |||||
| response.context["latest_question_list"], | |||||
| [question2, question1], | |||||
| ) | |||||
| @ -1,11 +0,0 @@ | |||||
| from django.urls import path | |||||
| from . import views | |||||
| app_name = "polls" | |||||
| urlpatterns = [ | |||||
| path("", views.IndexView.as_view(), name="index"), | |||||
| path("<int:pk>/", views.DetailView.as_view(), name="detail"), | |||||
| path("<int:pk>/results/", views.ResultsView.as_view(), name="results"), | |||||
| path("<int:question_id>/vote/", views.vote, name="vote"), | |||||
| ] | |||||
| @ -1,62 +0,0 @@ | |||||
| from django.shortcuts import get_object_or_404, render | |||||
| from django.http import HttpResponse, HttpResponseRedirect | |||||
| from django.urls import reverse | |||||
| from django.db.models import F | |||||
| from django.views import generic | |||||
| from django.utils import timezone | |||||
| from .models import Choice, Question | |||||
| # Create your views here. | |||||
| class IndexView(generic.ListView): | |||||
| template_name = "polls/index.html" | |||||
| context_object_name = "latest_question_list" | |||||
| def get_queryset(self): | |||||
| """ | |||||
| Return the last five published questions (not including those set to be | |||||
| published in the future). | |||||
| """ | |||||
| return Question.objects.filter(pub_date__lte=timezone.now()).order_by("-pub_date")[ | |||||
| :5 | |||||
| ] | |||||
| class DetailView(generic.DetailView): | |||||
| model = Question | |||||
| template_name = "polls/detail.html" | |||||
| def get_queryset(self): | |||||
| """ | |||||
| Excludes any questions that aren't published yet. | |||||
| """ | |||||
| return Question.objects.filter(pub_date__lte=timezone.now()) | |||||
| class ResultsView(generic.DetailView): | |||||
| model = Question | |||||
| template_name = "polls/results.html" | |||||
| def vote(request, question_id): | |||||
| question = get_object_or_404(Question, pk=question_id) | |||||
| try: | |||||
| selected_choice = question.choice_set.get(pk=request.POST["choice"]) | |||||
| except (KeyError, Choice.DoesNotExist): | |||||
| # Redisplay the question voting form. | |||||
| return render( | |||||
| request, | |||||
| "polls/detail.html", | |||||
| { | |||||
| "question": question, | |||||
| "error_message": "You didn't select a choice.", | |||||
| }, | |||||
| ) | |||||
| else: | |||||
| selected_choice.votes = F("votes") + 1 | |||||
| selected_choice.save() | |||||
| # Always return an HttpResponseRedirect after successfully dealing | |||||
| # with POST data. This prevents data from being posted twice if a | |||||
| # user hits the Back button. | |||||
| return HttpResponseRedirect(reverse("polls:results", args=(question.id,))) | |||||
| @ -1,12 +0,0 @@ | |||||
| {% extends "admin/base.html" %} | |||||
| {% block title %}{% if subtitle %}{{ subtitle }} | {% endif %}{{ title }} | {{ site_title|default:_('Django site admin') }}{% endblock %} | |||||
| {% block branding %} | |||||
| <div id="site-name"><a href="{% url 'admin:index' %}">Polls Administration</a></div> | |||||
| {% if user.is_anonymous %} | |||||
| {% include "admin/color_theme_toggle.html" %} | |||||
| {% endif %} | |||||
| {% endblock %} | |||||
| {% block nav-global %}{% endblock %} | |||||
| @ -0,0 +1,16 @@ | |||||
| [[source]] | |||||
| url = "https://pypi.org/simple" | |||||
| verify_ssl = true | |||||
| name = "pypi" | |||||
| [packages] | |||||
| django = "*" | |||||
| djangorestframework = "*" | |||||
| epub-meta = "*" | |||||
| django-db = "*" | |||||
| pillow = "*" | |||||
| [dev-packages] | |||||
| [requires] | |||||
| python_version = "3.10" | |||||
| @ -0,0 +1,172 @@ | |||||
| { | |||||
| "_meta": { | |||||
| "hash": { | |||||
| "sha256": "ef4cc3e12936cd221526bb3ceae7ac1b1b7593040426b99e3ef1b167fe85abf5" | |||||
| }, | |||||
| "pipfile-spec": 6, | |||||
| "requires": { | |||||
| "python_version": "3.10" | |||||
| }, | |||||
| "sources": [ | |||||
| { | |||||
| "name": "pypi", | |||||
| "url": "https://pypi.org/simple", | |||||
| "verify_ssl": true | |||||
| } | |||||
| ] | |||||
| }, | |||||
| "default": { | |||||
| "asgiref": { | |||||
| "hashes": [ | |||||
| "sha256:3e1e3ecc849832fe52ccf2cb6686b7a55f82bb1d6aee72a58826471390335e47", | |||||
| "sha256:c343bd80a0bec947a9860adb4c432ffa7db769836c64238fc34bdc3fec84d590" | |||||
| ], | |||||
| "markers": "python_version >= '3.8'", | |||||
| "version": "==3.8.1" | |||||
| }, | |||||
| "django": { | |||||
| "hashes": [ | |||||
| "sha256:bd4505cae0b9bd642313e8fb71810893df5dc2ffcacaa67a33af2d5cd61888f2", | |||||
| "sha256:f216510ace3de5de01329463a315a629f33480e893a9024fc93d8c32c22913da" | |||||
| ], | |||||
| "index": "pypi", | |||||
| "markers": "python_version >= '3.10'", | |||||
| "version": "==5.0.7" | |||||
| }, | |||||
| "django-db": { | |||||
| "hashes": [ | |||||
| "sha256:8639b6f78f04f34294cb465b308992a84297df41033519efd85b3a6b186b5296" | |||||
| ], | |||||
| "index": "pypi", | |||||
| "version": "==0.0.7" | |||||
| }, | |||||
| "djangorestframework": { | |||||
| "hashes": [ | |||||
| "sha256:2b8871b062ba1aefc2de01f773875441a961fefbf79f5eed1e32b2f096944b20", | |||||
| "sha256:36fe88cd2d6c6bec23dca9804bab2ba5517a8bb9d8f47ebc68981b56840107ad" | |||||
| ], | |||||
| "index": "pypi", | |||||
| "markers": "python_version >= '3.8'", | |||||
| "version": "==3.15.2" | |||||
| }, | |||||
| "epub-meta": { | |||||
| "hashes": [ | |||||
| "sha256:9d9c6afeaef796105a77d0aef33a1c943ccd91cccb2cb7716f59ad149819a4e9" | |||||
| ], | |||||
| "index": "pypi", | |||||
| "version": "==0.0.7" | |||||
| }, | |||||
| "pillow": { | |||||
| "hashes": [ | |||||
| "sha256:02a2be69f9c9b8c1e97cf2713e789d4e398c751ecfd9967c18d0ce304efbf885", | |||||
| "sha256:030abdbe43ee02e0de642aee345efa443740aa4d828bfe8e2eb11922ea6a21ea", | |||||
| "sha256:06b2f7898047ae93fad74467ec3d28fe84f7831370e3c258afa533f81ef7f3df", | |||||
| "sha256:0755ffd4a0c6f267cccbae2e9903d95477ca2f77c4fcf3a3a09570001856c8a5", | |||||
| "sha256:0a9ec697746f268507404647e531e92889890a087e03681a3606d9b920fbee3c", | |||||
| "sha256:0ae24a547e8b711ccaaf99c9ae3cd975470e1a30caa80a6aaee9a2f19c05701d", | |||||
| "sha256:134ace6dc392116566980ee7436477d844520a26a4b1bd4053f6f47d096997fd", | |||||
| "sha256:166c1cd4d24309b30d61f79f4a9114b7b2313d7450912277855ff5dfd7cd4a06", | |||||
| "sha256:1b5dea9831a90e9d0721ec417a80d4cbd7022093ac38a568db2dd78363b00908", | |||||
| "sha256:1d846aea995ad352d4bdcc847535bd56e0fd88d36829d2c90be880ef1ee4668a", | |||||
| "sha256:1ef61f5dd14c300786318482456481463b9d6b91ebe5ef12f405afbba77ed0be", | |||||
| "sha256:297e388da6e248c98bc4a02e018966af0c5f92dfacf5a5ca22fa01cb3179bca0", | |||||
| "sha256:298478fe4f77a4408895605f3482b6cc6222c018b2ce565c2b6b9c354ac3229b", | |||||
| "sha256:29dbdc4207642ea6aad70fbde1a9338753d33fb23ed6956e706936706f52dd80", | |||||
| "sha256:2db98790afc70118bd0255c2eeb465e9767ecf1f3c25f9a1abb8ffc8cfd1fe0a", | |||||
| "sha256:32cda9e3d601a52baccb2856b8ea1fc213c90b340c542dcef77140dfa3278a9e", | |||||
| "sha256:37fb69d905be665f68f28a8bba3c6d3223c8efe1edf14cc4cfa06c241f8c81d9", | |||||
| "sha256:416d3a5d0e8cfe4f27f574362435bc9bae57f679a7158e0096ad2beb427b8696", | |||||
| "sha256:43efea75eb06b95d1631cb784aa40156177bf9dd5b4b03ff38979e048258bc6b", | |||||
| "sha256:4b35b21b819ac1dbd1233317adeecd63495f6babf21b7b2512d244ff6c6ce309", | |||||
| "sha256:4d9667937cfa347525b319ae34375c37b9ee6b525440f3ef48542fcf66f2731e", | |||||
| "sha256:5161eef006d335e46895297f642341111945e2c1c899eb406882a6c61a4357ab", | |||||
| "sha256:543f3dc61c18dafb755773efc89aae60d06b6596a63914107f75459cf984164d", | |||||
| "sha256:551d3fd6e9dc15e4c1eb6fc4ba2b39c0c7933fa113b220057a34f4bb3268a060", | |||||
| "sha256:59291fb29317122398786c2d44427bbd1a6d7ff54017075b22be9d21aa59bd8d", | |||||
| "sha256:5b001114dd152cfd6b23befeb28d7aee43553e2402c9f159807bf55f33af8a8d", | |||||
| "sha256:5b4815f2e65b30f5fbae9dfffa8636d992d49705723fe86a3661806e069352d4", | |||||
| "sha256:5dc6761a6efc781e6a1544206f22c80c3af4c8cf461206d46a1e6006e4429ff3", | |||||
| "sha256:5e84b6cc6a4a3d76c153a6b19270b3526a5a8ed6b09501d3af891daa2a9de7d6", | |||||
| "sha256:6209bb41dc692ddfee4942517c19ee81b86c864b626dbfca272ec0f7cff5d9fb", | |||||
| "sha256:673655af3eadf4df6b5457033f086e90299fdd7a47983a13827acf7459c15d94", | |||||
| "sha256:6c762a5b0997f5659a5ef2266abc1d8851ad7749ad9a6a5506eb23d314e4f46b", | |||||
| "sha256:7086cc1d5eebb91ad24ded9f58bec6c688e9f0ed7eb3dbbf1e4800280a896496", | |||||
| "sha256:73664fe514b34c8f02452ffb73b7a92c6774e39a647087f83d67f010eb9a0cf0", | |||||
| "sha256:76a911dfe51a36041f2e756b00f96ed84677cdeb75d25c767f296c1c1eda1319", | |||||
| "sha256:780c072c2e11c9b2c7ca37f9a2ee8ba66f44367ac3e5c7832afcfe5104fd6d1b", | |||||
| "sha256:7928ecbf1ece13956b95d9cbcfc77137652b02763ba384d9ab508099a2eca856", | |||||
| "sha256:7970285ab628a3779aecc35823296a7869f889b8329c16ad5a71e4901a3dc4ef", | |||||
| "sha256:7a8d4bade9952ea9a77d0c3e49cbd8b2890a399422258a77f357b9cc9be8d680", | |||||
| "sha256:7c1ee6f42250df403c5f103cbd2768a28fe1a0ea1f0f03fe151c8741e1469c8b", | |||||
| "sha256:7dfecdbad5c301d7b5bde160150b4db4c659cee2b69589705b6f8a0c509d9f42", | |||||
| "sha256:812f7342b0eee081eaec84d91423d1b4650bb9828eb53d8511bcef8ce5aecf1e", | |||||
| "sha256:866b6942a92f56300012f5fbac71f2d610312ee65e22f1aa2609e491284e5597", | |||||
| "sha256:86dcb5a1eb778d8b25659d5e4341269e8590ad6b4e8b44d9f4b07f8d136c414a", | |||||
| "sha256:87dd88ded2e6d74d31e1e0a99a726a6765cda32d00ba72dc37f0651f306daaa8", | |||||
| "sha256:8bc1a764ed8c957a2e9cacf97c8b2b053b70307cf2996aafd70e91a082e70df3", | |||||
| "sha256:8d4d5063501b6dd4024b8ac2f04962d661222d120381272deea52e3fc52d3736", | |||||
| "sha256:8f0aef4ef59694b12cadee839e2ba6afeab89c0f39a3adc02ed51d109117b8da", | |||||
| "sha256:930044bb7679ab003b14023138b50181899da3f25de50e9dbee23b61b4de2126", | |||||
| "sha256:950be4d8ba92aca4b2bb0741285a46bfae3ca699ef913ec8416c1b78eadd64cd", | |||||
| "sha256:961a7293b2457b405967af9c77dcaa43cc1a8cd50d23c532e62d48ab6cdd56f5", | |||||
| "sha256:9b885f89040bb8c4a1573566bbb2f44f5c505ef6e74cec7ab9068c900047f04b", | |||||
| "sha256:9f4727572e2918acaa9077c919cbbeb73bd2b3ebcfe033b72f858fc9fbef0026", | |||||
| "sha256:a02364621fe369e06200d4a16558e056fe2805d3468350df3aef21e00d26214b", | |||||
| "sha256:a985e028fc183bf12a77a8bbf36318db4238a3ded7fa9df1b9a133f1cb79f8fc", | |||||
| "sha256:ac1452d2fbe4978c2eec89fb5a23b8387aba707ac72810d9490118817d9c0b46", | |||||
| "sha256:b15e02e9bb4c21e39876698abf233c8c579127986f8207200bc8a8f6bb27acf2", | |||||
| "sha256:b2724fdb354a868ddf9a880cb84d102da914e99119211ef7ecbdc613b8c96b3c", | |||||
| "sha256:bbc527b519bd3aa9d7f429d152fea69f9ad37c95f0b02aebddff592688998abe", | |||||
| "sha256:bcd5e41a859bf2e84fdc42f4edb7d9aba0a13d29a2abadccafad99de3feff984", | |||||
| "sha256:bd2880a07482090a3bcb01f4265f1936a903d70bc740bfcb1fd4e8a2ffe5cf5a", | |||||
| "sha256:bee197b30783295d2eb680b311af15a20a8b24024a19c3a26431ff83eb8d1f70", | |||||
| "sha256:bf2342ac639c4cf38799a44950bbc2dfcb685f052b9e262f446482afaf4bffca", | |||||
| "sha256:c76e5786951e72ed3686e122d14c5d7012f16c8303a674d18cdcd6d89557fc5b", | |||||
| "sha256:cbed61494057c0f83b83eb3a310f0bf774b09513307c434d4366ed64f4128a91", | |||||
| "sha256:cfdd747216947628af7b259d274771d84db2268ca062dd5faf373639d00113a3", | |||||
| "sha256:d7480af14364494365e89d6fddc510a13e5a2c3584cb19ef65415ca57252fb84", | |||||
| "sha256:dbc6ae66518ab3c5847659e9988c3b60dc94ffb48ef9168656e0019a93dbf8a1", | |||||
| "sha256:dc3e2db6ba09ffd7d02ae9141cfa0ae23393ee7687248d46a7507b75d610f4f5", | |||||
| "sha256:dfe91cb65544a1321e631e696759491ae04a2ea11d36715eca01ce07284738be", | |||||
| "sha256:e4d49b85c4348ea0b31ea63bc75a9f3857869174e2bf17e7aba02945cd218e6f", | |||||
| "sha256:e4db64794ccdf6cb83a59d73405f63adbe2a1887012e308828596100a0b2f6cc", | |||||
| "sha256:e553cad5179a66ba15bb18b353a19020e73a7921296a7979c4a2b7f6a5cd57f9", | |||||
| "sha256:e88d5e6ad0d026fba7bdab8c3f225a69f063f116462c49892b0149e21b6c0a0e", | |||||
| "sha256:ecd85a8d3e79cd7158dec1c9e5808e821feea088e2f69a974db5edf84dc53141", | |||||
| "sha256:f5b92f4d70791b4a67157321c4e8225d60b119c5cc9aee8ecf153aace4aad4ef", | |||||
| "sha256:f5f0c3e969c8f12dd2bb7e0b15d5c468b51e5017e01e2e867335c81903046a22", | |||||
| "sha256:f7baece4ce06bade126fb84b8af1c33439a76d8a6fd818970215e0560ca28c27", | |||||
| "sha256:ff25afb18123cea58a591ea0244b92eb1e61a1fd497bf6d6384f09bc3262ec3e", | |||||
| "sha256:ff337c552345e95702c5fde3158acb0625111017d0e5f24bf3acdb9cc16b90d1" | |||||
| ], | |||||
| "index": "pypi", | |||||
| "markers": "python_version >= '3.8'", | |||||
| "version": "==10.4.0" | |||||
| }, | |||||
| "pymysql": { | |||||
| "hashes": [ | |||||
| "sha256:4de15da4c61dc132f4fb9ab763063e693d521a80fd0e87943b9a453dd4c19d6c", | |||||
| "sha256:e127611aaf2b417403c60bf4dc570124aeb4a57f5f37b8e95ae399a42f904cd0" | |||||
| ], | |||||
| "markers": "python_version >= '3.7'", | |||||
| "version": "==1.1.1" | |||||
| }, | |||||
| "sqlparse": { | |||||
| "hashes": [ | |||||
| "sha256:773dcbf9a5ab44a090f3441e2180efe2560220203dc2f8c0b0fa141e18b505e4", | |||||
| "sha256:bb6b4df465655ef332548e24f08e205afc81b9ab86cb1c45657a7ff173a3a00e" | |||||
| ], | |||||
| "markers": "python_version >= '3.8'", | |||||
| "version": "==0.5.1" | |||||
| }, | |||||
| "typing-extensions": { | |||||
| "hashes": [ | |||||
| "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", | |||||
| "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8" | |||||
| ], | |||||
| "markers": "python_version < '3.11'", | |||||
| "version": "==4.12.2" | |||||
| } | |||||
| }, | |||||
| "develop": {} | |||||
| } | |||||
| @ -0,0 +1,9 @@ | |||||
| from django.contrib import admin | |||||
| # Register your models here. | |||||
| from .models import Autor, Libro | |||||
| admin.site.register(Autor) | |||||
| admin.site.register(Libro) | |||||
| @ -1,6 +1,6 @@ | |||||
| from django.apps import AppConfig | from django.apps import AppConfig | ||||
| class PollsConfig(AppConfig): | |||||
| class GestionConfig(AppConfig): | |||||
| default_auto_field = 'django.db.models.BigAutoField' | default_auto_field = 'django.db.models.BigAutoField' | ||||
| name = 'polls' | |||||
| name = 'gestion' | |||||
| @ -0,0 +1,12 @@ | |||||
| from django import forms | |||||
| from .models import Autor, Libro | |||||
| class AutorForm(forms.ModelForm): | |||||
| class Meta: | |||||
| model = Autor | |||||
| fields = ['nombre', 'biografia', 'foto'] | |||||
| class LibroForm(forms.ModelForm): | |||||
| class Meta: | |||||
| model = Libro | |||||
| fields = ['titulo', 'autor', 'fecha_publicacion', 'descripcion', 'archivo', 'portada'] | |||||
| @ -0,0 +1,34 @@ | |||||
| # Generated by Django 5.0.7 on 2024-08-05 09:29 | |||||
| import django.db.models.deletion | |||||
| from django.db import migrations, models | |||||
| class Migration(migrations.Migration): | |||||
| initial = True | |||||
| dependencies = [ | |||||
| ] | |||||
| operations = [ | |||||
| migrations.CreateModel( | |||||
| name='Autor', | |||||
| fields=[ | |||||
| ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), | |||||
| ('nombre', models.CharField(max_length=200)), | |||||
| ('biografia', models.TextField(blank=True, null=True)), | |||||
| ], | |||||
| ), | |||||
| migrations.CreateModel( | |||||
| name='Libro', | |||||
| fields=[ | |||||
| ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), | |||||
| ('titulo', models.CharField(max_length=200)), | |||||
| ('fecha_publicacion', models.DateField()), | |||||
| ('descripcion', models.TextField(blank=True, null=True)), | |||||
| ('archivo', models.FileField(upload_to='libros/')), | |||||
| ('autor', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='gestion.autor')), | |||||
| ], | |||||
| ), | |||||
| ] | |||||
| @ -0,0 +1,20 @@ | |||||
| # Generated by Django 5.0.7 on 2024-08-05 10:17 | |||||
| import django.core.validators | |||||
| import gestion.models | |||||
| from django.db import migrations, models | |||||
| class Migration(migrations.Migration): | |||||
| dependencies = [ | |||||
| ('gestion', '0001_initial'), | |||||
| ] | |||||
| operations = [ | |||||
| migrations.AlterField( | |||||
| model_name='libro', | |||||
| name='fecha_publicacion', | |||||
| field=models.PositiveBigIntegerField(default=2024, validators=[django.core.validators.MinValueValidator(1984), gestion.models.max_value_current_year]), | |||||
| ), | |||||
| ] | |||||
| @ -0,0 +1,18 @@ | |||||
| # Generated by Django 5.0.7 on 2024-08-05 10:24 | |||||
| from django.db import migrations, models | |||||
| class Migration(migrations.Migration): | |||||
| dependencies = [ | |||||
| ('gestion', '0002_alter_libro_fecha_publicacion'), | |||||
| ] | |||||
| operations = [ | |||||
| migrations.AddField( | |||||
| model_name='libro', | |||||
| name='portada', | |||||
| field=models.ImageField(blank=True, null=True, upload_to='portadas/'), | |||||
| ), | |||||
| ] | |||||
| @ -0,0 +1,18 @@ | |||||
| # Generated by Django 5.0.7 on 2024-08-05 10:29 | |||||
| from django.db import migrations, models | |||||
| class Migration(migrations.Migration): | |||||
| dependencies = [ | |||||
| ('gestion', '0003_libro_portada'), | |||||
| ] | |||||
| operations = [ | |||||
| migrations.AddField( | |||||
| model_name='autor', | |||||
| name='foto', | |||||
| field=models.ImageField(blank=True, null=True, upload_to='autores/'), | |||||
| ), | |||||
| ] | |||||
| @ -0,0 +1,29 @@ | |||||
| from django.db import models | |||||
| import datetime | |||||
| from django.core.validators import MaxValueValidator, MinValueValidator | |||||
| def current_year(): | |||||
| return datetime.date.today().year | |||||
| def max_value_current_year(value): | |||||
| return MaxValueValidator(current_year())(value) | |||||
| class Autor(models.Model): | |||||
| nombre = models.CharField(max_length=200) | |||||
| biografia = models.TextField(blank=True, null=True) | |||||
| foto = models.ImageField(upload_to='autores/', blank=True, null=True) # Nuevo campo | |||||
| def __str__(self): | |||||
| return self.nombre | |||||
| class Libro(models.Model): | |||||
| titulo = models.CharField(max_length=200) | |||||
| autor = models.ForeignKey(Autor, on_delete=models.CASCADE) | |||||
| fecha_publicacion = models.PositiveBigIntegerField(default=current_year(), validators=[MinValueValidator(1984), max_value_current_year]) | |||||
| descripcion = models.TextField(blank=True, null=True) | |||||
| archivo = models.FileField(upload_to='libros/') | |||||
| portada = models.ImageField(upload_to='portadas/', blank=True, null=True) # Nuevo campo | |||||
| def __str__(self): | |||||
| return self.titulo | |||||
| @ -0,0 +1,14 @@ | |||||
| <!DOCTYPE html> | |||||
| <html> | |||||
| <head> | |||||
| <title>Detalle del Autor</title> | |||||
| </head> | |||||
| <body> | |||||
| <h1>{{ autor.nombre }}</h1> | |||||
| <p>{{ autor.biografia }}</p> | |||||
| {% if autor.foto %} | |||||
| <img src="{{ autor.foto.url }}" alt="Foto del autor" width="200"> | |||||
| {% endif %} | |||||
| <a href="{% url 'lista_autores' %}">Volver a la lista de autores</a> | |||||
| </body> | |||||
| </html> | |||||
| @ -0,0 +1,18 @@ | |||||
| <!DOCTYPE html> | |||||
| <html> | |||||
| <head> | |||||
| <title>Detalle del Libro</title> | |||||
| </head> | |||||
| <body> | |||||
| <h1>{{ libro.titulo }}</h1> | |||||
| <p>{{ libro.descripcion }}</p> | |||||
| <p><strong>Autor:</strong> {{ libro.autor.nombre }}</p> | |||||
| <p><strong>Fecha de Publicación:</strong> {{ libro.fecha_publicacion }}</p> | |||||
| {% if libro.portada %} | |||||
| <img src="{{ libro.portada.url }}" alt="Portada del libro" width="200"> | |||||
| {% endif %} | |||||
| {% if libro.archivo %} | |||||
| <p><a href="{{ libro.archivo.url }}">Descargar</a></p> | |||||
| {% endif %} <a href="{% url 'lista_libros' %}">Volver a la lista de libros</a> | |||||
| </body> | |||||
| </html> | |||||
| @ -0,0 +1,15 @@ | |||||
| <!DOCTYPE html> | |||||
| <html> | |||||
| <head> | |||||
| <title>Formulario de Autor</title> | |||||
| </head> | |||||
| <body> | |||||
| <h1>{% if form.instance.pk %}Editar Autor{% else %}Nuevo Autor{% endif %}</h1> | |||||
| <form method="post" enctype="multipart/form-data"> | |||||
| {% csrf_token %} | |||||
| {{ form.as_p }} | |||||
| <button type="submit">Guardar</button> | |||||
| </form> | |||||
| <a href="{% url 'lista_autores' %}">Cancelar</a> | |||||
| </body> | |||||
| </html> | |||||
| @ -0,0 +1,15 @@ | |||||
| <!DOCTYPE html> | |||||
| <html> | |||||
| <head> | |||||
| <title>Formulario de Libro</title> | |||||
| </head> | |||||
| <body> | |||||
| <h1>{% if form.instance.pk %}Editar Libro{% else %}Nuevo Libro{% endif %}</h1> | |||||
| <form method="post" enctype="multipart/form-data"> | |||||
| {% csrf_token %} | |||||
| {{ form.as_p }} | |||||
| <button type="submit">Guardar</button> | |||||
| </form> | |||||
| <a href="{% url 'lista_libros' %}">Cancelar</a> | |||||
| </body> | |||||
| </html> | |||||
| @ -0,0 +1,19 @@ | |||||
| <!DOCTYPE html> | |||||
| <html> | |||||
| <head> | |||||
| <title>Lista de Autores</title> | |||||
| </head> | |||||
| <body> | |||||
| <h1>Lista de Autores</h1> | |||||
| <a href="{% url 'nuevo_autor' %}">Nuevo Autor</a> | |||||
| <ul> | |||||
| {% for autor in autores %} | |||||
| <li> | |||||
| <a href="{% url 'detalle_autor' autor.id %}">{{ autor.nombre }}</a> | |||||
| <a href="{% url 'editar_autor' autor.id %}">Editar</a> | |||||
| <a href="{% url 'eliminar_autor' autor.id %}">Eliminar</a> | |||||
| </li> | |||||
| {% endfor %} | |||||
| </ul> | |||||
| </body> | |||||
| </html> | |||||
| @ -0,0 +1,19 @@ | |||||
| <!DOCTYPE html> | |||||
| <html> | |||||
| <head> | |||||
| <title>Lista de Libros</title> | |||||
| </head> | |||||
| <body> | |||||
| <h1>Lista de Libros</h1> | |||||
| <a href="{% url 'nuevo_libro' %}">Nuevo Libro</a> | |||||
| <ul> | |||||
| {% for libro in libros %} | |||||
| <li> | |||||
| <a href="{% url 'detalle_libro' libro.id %}">{{ libro.titulo }}</a> | |||||
| <a href="{% url 'editar_libro' libro.id %}">Editar</a> | |||||
| <a href="{% url 'eliminar_libro' libro.id %}">Eliminar</a> | |||||
| </li> | |||||
| {% endfor %} | |||||
| </ul> | |||||
| </body> | |||||
| </html> | |||||
| @ -0,0 +1,3 @@ | |||||
| from django.test import TestCase | |||||
| # Create your tests here. | |||||
| @ -0,0 +1,16 @@ | |||||
| from django.urls import path | |||||
| from . import views | |||||
| urlpatterns = [ | |||||
| path('autores/', views.lista_autores, name='lista_autores'), | |||||
| path('autores/nuevo/', views.nuevo_autor, name='nuevo_autor'), | |||||
| path('autores/<int:autor_id>/', views.detalle_autor, name='detalle_autor'), | |||||
| path('autores/<int:autor_id>/editar/', views.editar_autor, name='editar_autor'), | |||||
| path('autores/<int:autor_id>/eliminar/', views.eliminar_autor, name='eliminar_autor'), | |||||
| path('libros/', views.lista_libros, name='lista_libros'), | |||||
| path('libros/nuevo/', views.nuevo_libro, name='nuevo_libro'), | |||||
| path('libros/<int:libro_id>/', views.detalle_libro, name='detalle_libro'), | |||||
| path('libros/<int:libro_id>/editar/', views.editar_libro, name='editar_libro'), | |||||
| path('libros/<int:libro_id>/eliminar/', views.eliminar_libro, name='eliminar_libro'), | |||||
| ] | |||||
| @ -0,0 +1,73 @@ | |||||
| from django.shortcuts import render, get_object_or_404, redirect | |||||
| from .models import Autor, Libro | |||||
| from .forms import AutorForm, LibroForm | |||||
| # Vistas para los autores | |||||
| def lista_autores(request): | |||||
| autores = Autor.objects.all() | |||||
| return render(request, 'gestion/lista_autores.html', {'autores': autores}) | |||||
| def detalle_autor(request, autor_id): | |||||
| autor = get_object_or_404(Autor, pk=autor_id) | |||||
| return render(request, 'gestion/detalle_autor.html', {'autor': autor}) | |||||
| def nuevo_autor(request): | |||||
| if request.method == 'POST': | |||||
| form = AutorForm(request.POST, request.FILES) | |||||
| if form.is_valid(): | |||||
| form.save() | |||||
| return redirect('lista_autores') | |||||
| else: | |||||
| form = AutorForm() | |||||
| return render(request, 'gestion/form_autor.html', {'form': form}) | |||||
| def editar_autor(request, autor_id): | |||||
| autor = get_object_or_404(Autor, pk=autor_id) | |||||
| if request.method == 'POST': | |||||
| form = AutorForm(request.POST, request.FILES, instance=autor) | |||||
| if form.is_valid(): | |||||
| form.save() | |||||
| return redirect('lista_autores') | |||||
| else: | |||||
| form = AutorForm(instance=autor) | |||||
| return render(request, 'gestion/form_autor.html', {'form': form}) | |||||
| def eliminar_autor(request, autor_id): | |||||
| autor = get_object_or_404(Autor, pk=autor_id) | |||||
| autor.delete() | |||||
| return redirect('lista_autores') | |||||
| # Vistas para los libros | |||||
| def lista_libros(request): | |||||
| libros = Libro.objects.all() | |||||
| return render(request, 'gestion/lista_libros.html', {'libros': libros}) | |||||
| def detalle_libro(request, libro_id): | |||||
| libro = get_object_or_404(Libro, pk=libro_id) | |||||
| return render(request, 'gestion/detalle_libro.html', {'libro': libro}) | |||||
| def nuevo_libro(request): | |||||
| if request.method == 'POST': | |||||
| form = LibroForm(request.POST, request.FILES) | |||||
| if form.is_valid(): | |||||
| form.save() | |||||
| return redirect('lista_libros') | |||||
| else: | |||||
| form = LibroForm() | |||||
| return render(request, 'gestion/form_libro.html', {'form': form}) | |||||
| def editar_libro(request, libro_id): | |||||
| libro = get_object_or_404(Libro, pk=libro_id) | |||||
| if request.method == 'POST': | |||||
| form = LibroForm(request.POST, request.FILES, instance=libro) | |||||
| if form.is_valid(): | |||||
| form.save() | |||||
| return redirect('lista_libros') | |||||
| else: | |||||
| form = LibroForm(instance=libro) | |||||
| return render(request, 'gestion/form_libro.html', {'form': form}) | |||||
| def eliminar_libro(request, libro_id): | |||||
| libro = get_object_or_404(Libro, pk=libro_id) | |||||
| libro.delete() | |||||
| return redirect('lista_libros') | |||||