Compare commits
	
		
			1 commit
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 479c9eb192 | 
					 18 changed files with 706 additions and 0 deletions
				
			
		
							
								
								
									
										0
									
								
								framadate/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								framadate/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										3
									
								
								framadate/admin.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								framadate/admin.py
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,3 @@ | |||
| from django.contrib import admin | ||||
| 
 | ||||
| # Register your models here. | ||||
							
								
								
									
										6
									
								
								framadate/apps.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								framadate/apps.py
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,6 @@ | |||
| from django.apps import AppConfig | ||||
| 
 | ||||
| 
 | ||||
| class FramadateConfig(AppConfig): | ||||
|     default_auto_field = 'django.db.models.BigAutoField' | ||||
|     name = 'framadate' | ||||
							
								
								
									
										54
									
								
								framadate/migrations/0001_initial.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										54
									
								
								framadate/migrations/0001_initial.py
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,54 @@ | |||
| # Generated by Django 4.2.16 on 2025-01-26 17:14 | ||||
| 
 | ||||
| from django.db import migrations, models | ||||
| import django.db.models.deletion | ||||
| 
 | ||||
| 
 | ||||
| class Migration(migrations.Migration): | ||||
| 
 | ||||
|     initial = True | ||||
| 
 | ||||
|     dependencies = [ | ||||
|     ] | ||||
| 
 | ||||
|     operations = [ | ||||
|         migrations.CreateModel( | ||||
|             name='Framadate', | ||||
|             fields=[ | ||||
|                 ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), | ||||
|                 ('name', models.TextField()), | ||||
|             ], | ||||
|         ), | ||||
|         migrations.CreateModel( | ||||
|             name='FramadateDate', | ||||
|             fields=[ | ||||
|                 ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), | ||||
|                 ('day', models.DateField()), | ||||
|                 ('time', models.TimeField(blank=True, null=True)), | ||||
|                 ('framadate', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='days_set', to='framadate.framadate')), | ||||
|             ], | ||||
|             options={ | ||||
|                 'ordering': ['day', 'time'], | ||||
|             }, | ||||
|         ), | ||||
|         migrations.CreateModel( | ||||
|             name='NameInFramadate', | ||||
|             fields=[ | ||||
|                 ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), | ||||
|                 ('name', models.CharField(max_length=200)), | ||||
|                 ('framadate', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='names_set', to='framadate.framadate')), | ||||
|             ], | ||||
|         ), | ||||
|         migrations.CreateModel( | ||||
|             name='Record', | ||||
|             fields=[ | ||||
|                 ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), | ||||
|                 ('choice', models.IntegerField(choices=[(0, 'Unknown'), (3, 'Yes'), (2, 'No'), (1, 'Maybe')], default=0)), | ||||
|                 ('framadateDate', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='records_set', to='framadate.framadatedate')), | ||||
|                 ('nameInFramadate', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='records_set', to='framadate.nameinframadate')), | ||||
|             ], | ||||
|             options={ | ||||
|                 'ordering': ['framadateDate__day', 'framadateDate__time'], | ||||
|             }, | ||||
|         ), | ||||
|     ] | ||||
							
								
								
									
										0
									
								
								framadate/migrations/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								framadate/migrations/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										59
									
								
								framadate/models.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										59
									
								
								framadate/models.py
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,59 @@ | |||
| from django.db import models | ||||
| from datetime import datetime | ||||
| 
 | ||||
| # Create your models here | ||||
| class Framadate(models.Model): | ||||
|     name = models.TextField() | ||||
| 
 | ||||
|     "isRelevant if all dates are in the future" | ||||
|     @property | ||||
|     def isRelevant(self): | ||||
|         for day in self.days_set.all(): | ||||
|             if day.day < datetime.now().date(): | ||||
|                 return False | ||||
|         return True | ||||
| 
 | ||||
|     def __str__(self): | ||||
|         return self.name | ||||
| 
 | ||||
| class NameInFramadate(models.Model): | ||||
|     framadate = models.ForeignKey(Framadate, on_delete=models.CASCADE, related_name="names_set") | ||||
|     name = models.CharField(max_length=200) | ||||
| 
 | ||||
|     def __str__(self): | ||||
|         return self.name + " - in - " + self.framadate.name | ||||
|      | ||||
| class FramadateDate(models.Model): | ||||
|     framadate = models.ForeignKey(Framadate, on_delete=models.CASCADE, related_name="days_set") | ||||
|     day = models.DateField() | ||||
|     time = models.TimeField(null=True, blank=True) | ||||
| 
 | ||||
|     def __str__(self): | ||||
|         return self.day.strftime("%Y-%m-%d") + " - in- " + self.framadate.name | ||||
|      | ||||
|     class Meta: | ||||
|         ordering = ['day', 'time'] | ||||
| 
 | ||||
| class Choice(models.IntegerChoices): | ||||
|     UNKNOWN = 0 | ||||
|     YES = 3 | ||||
|     NO = 2 | ||||
|     MAYBE = 1 | ||||
| 
 | ||||
| # Quirk -> name and framadateDate needs to share same Framadate... | ||||
| class Record(models.Model): | ||||
|     nameInFramadate = models.ForeignKey(NameInFramadate, on_delete=models.CASCADE, related_name="records_set") | ||||
|     framadateDate = models.ForeignKey(FramadateDate, on_delete=models.CASCADE, related_name="records_set") | ||||
|     choice = models.IntegerField(choices=Choice.choices, default=Choice.UNKNOWN) | ||||
| 
 | ||||
|     @property | ||||
|     def choiceName(self): | ||||
|         return Choice(self.choice).name | ||||
| 
 | ||||
|     def __str__(self): | ||||
|         if self.framadateDate.time: | ||||
|             return self.nameInFramadate.name + " - in - " + self.framadateDate.framadate.name + " - " + self.framadateDate.day.strftime("%Y-%m-%d") + " - " + self.framadateDate.time.strftime("%H") + ":00 - " + Choice(self.choice).name | ||||
|         return self.nameInFramadate.name + " - in - " + self.framadateDate.framadate.name + " - " + self.framadateDate.day.strftime("%Y-%m-%d") + " - " + Choice(self.choice).name | ||||
| 
 | ||||
|     class Meta: | ||||
|         ordering = ['framadateDate__day', 'framadateDate__time'] | ||||
							
								
								
									
										132
									
								
								framadate/static/framadate/framadate_detail.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										132
									
								
								framadate/static/framadate/framadate_detail.css
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,132 @@ | |||
| 
 | ||||
| textarea { | ||||
|     font-family: "Tomorrow", serif; | ||||
|     font-weight: 400; | ||||
|     font-style: normal; | ||||
|     font-size: 1.5em; | ||||
|     padding: 0.3em 0.5em; | ||||
| } | ||||
| 
 | ||||
| .framadate-detail { | ||||
|     display: flex; | ||||
|     flex-direction: column; | ||||
|     gap: 0.8em; | ||||
|     overflow-x: hidden; | ||||
| } | ||||
| 
 | ||||
| .table { | ||||
|     overflow-x: auto; | ||||
|     padding-bottom: 2em; | ||||
|     display: flex; | ||||
|     flex-direction: column; | ||||
|     gap: 0.3em; | ||||
| 
 | ||||
|     .row { | ||||
|         display: flex; | ||||
|         gap: 0.3em; | ||||
|         width: max-content; | ||||
| 
 | ||||
|         .item { | ||||
|             /* background-color: red; */ | ||||
|             width: 140px; | ||||
|             text-align: center; | ||||
|             padding: 0.8em 0; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     .YES { | ||||
|         background-color: green; | ||||
|     } | ||||
| 
 | ||||
|     .NO { | ||||
|         background-color: red; | ||||
|     } | ||||
| 
 | ||||
|     .MAYBE { | ||||
|         background-color: goldenrod; | ||||
|     } | ||||
| 
 | ||||
|     .UNKNOWN { | ||||
|         background-color: gray; | ||||
|     } | ||||
| 
 | ||||
|     .radio-column { | ||||
|         display: flex; | ||||
|         flex-direction: column; | ||||
|         align-items: start; | ||||
|          | ||||
|         label { | ||||
|             width: 100%; | ||||
|             padding: 0.5em 0; | ||||
|         } | ||||
| 
 | ||||
|         input { | ||||
|             display: none; | ||||
|         } | ||||
| 
 | ||||
|         label:has(input:checked) { | ||||
|             background-color: #2973B2; | ||||
|             color: white; | ||||
|             border-radius: 10px; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     #name { | ||||
|         font-family: "Tomorrow", serif;  | ||||
|         font-size: 1em; | ||||
|     } | ||||
| 
 | ||||
|     .submit { | ||||
|         transition: box-shadow 0.2s ease; | ||||
|         background-color: #2973B2; | ||||
|         color: white; | ||||
|         text-decoration: none; | ||||
|         padding: 0.5em 1em; | ||||
|         margin: 1em; | ||||
|         width: min-content; | ||||
|         white-space: nowrap; | ||||
|         border-radius: 20px; | ||||
|         box-shadow: 0 0 10px rgba(0, 0, 0, 0.5); | ||||
|         border: none; | ||||
|         font-size: 1.4em; | ||||
|         font-family: "Tomorrow", serif; | ||||
|         width: 150px; | ||||
|     } | ||||
| 
 | ||||
|     .delete { | ||||
|         background-color: red; | ||||
|         color: black; | ||||
|          | ||||
|     } | ||||
| 
 | ||||
|     .submit:hover { | ||||
|         box-shadow: none; | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| .center { | ||||
|     display: flex; | ||||
|     align-items: center; | ||||
|     justify-content: center; | ||||
| } | ||||
| 
 | ||||
| .mini-row .item { | ||||
|     font-size: 0.8em; | ||||
| } | ||||
| 
 | ||||
| .item-with-edit { | ||||
|     display: flex; | ||||
|     align-items: center; | ||||
|     justify-content: center; | ||||
|     gap: 1em; | ||||
| 
 | ||||
|     a { | ||||
|         color: #2973B2; | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| .delete-framadate { | ||||
|     font-size: 0.5em; | ||||
|     margin-left: 2em; | ||||
|     color: red; | ||||
| } | ||||
							
								
								
									
										82
									
								
								framadate/static/framadate/new_framadate.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										82
									
								
								framadate/static/framadate/new_framadate.css
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,82 @@ | |||
| .label-name { | ||||
|     display: flex; | ||||
|     flex-direction: row; | ||||
|     gap: 2em; | ||||
|     align-items: center; | ||||
|     font-family: "Tomorrow", serif; | ||||
| 
 | ||||
|     input { | ||||
|         font-family: "Tomorrow", serif; | ||||
|         font-size: 1em; | ||||
|         padding: 0.5em; | ||||
|         border-radius: 10px; | ||||
|         text-align: center; | ||||
|         border: 1px solid #606060; | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| .dates { | ||||
|     border: 1px solid #ccc; | ||||
|     padding: 1em; | ||||
|     margin: 1em 0; | ||||
|     border-radius: 10px; | ||||
|     display: flex; | ||||
|     flex-direction: column; | ||||
|     gap: 0.5em; | ||||
| 
 | ||||
|     .date { | ||||
|         display: flex; | ||||
|         flex-direction: row; | ||||
|         gap: 2em; | ||||
|         align-items: center; | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| input[type="date"], input[type="time"] { | ||||
|     font-family: "Tomorrow", serif; | ||||
|         font-size: 1em; | ||||
|         padding: 0.3em 0.7em; | ||||
|         border: 1px solid #606060; | ||||
|         border-radius: 10px; | ||||
|         margin-left: 0.5em; | ||||
| } | ||||
| 
 | ||||
| .controls { | ||||
|     margin-bottom: 2em; | ||||
|     display: flex; | ||||
|     flex-direction: column; | ||||
|     gap: 0.5em; | ||||
|     align-items: start; | ||||
| } | ||||
| 
 | ||||
| .add-button { | ||||
|     border: none; | ||||
|     background-color: #2973B2; | ||||
|     color: white; | ||||
|     padding: 0.4em 1em; | ||||
|     font-size: 1em; | ||||
|     font-family: "Tomorrow", serif; | ||||
|     border-radius: 10px; | ||||
|     box-shadow: 0 0 10px rgba(0, 0, 0, 0.5); | ||||
|     transition: box-shadow 0.2s ease; | ||||
|     margin: 0.5em; | ||||
| } | ||||
| 
 | ||||
| .add-button:hover { | ||||
|     box-shadow: none; | ||||
| } | ||||
| 
 | ||||
| .span-controls { | ||||
|     display: flex; | ||||
|     gap: 2em; | ||||
| 
 | ||||
|     label { | ||||
|         display: flex; | ||||
|         align-items: center; | ||||
|         gap: 1em; | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| .create { | ||||
|     background-color: green; | ||||
| } | ||||
							
								
								
									
										47
									
								
								framadate/static/framadate/style.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								framadate/static/framadate/style.css
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,47 @@ | |||
| body { | ||||
|     font-family: "Tomorrow", serif; | ||||
|     font-weight: 400; | ||||
|     font-style: normal; | ||||
|     margin: 1em; | ||||
|     padding: 1em; | ||||
|     border: 1px solid black; | ||||
|     border-radius: 20px; | ||||
|     font-size: 1.2em; | ||||
| 
 | ||||
|     .framadates { | ||||
|         display: flex; | ||||
|         flex-direction: column; | ||||
|         gap: 0.8em; | ||||
| 
 | ||||
|         a { | ||||
|             color: black; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     h1 { | ||||
|         margin: 0.3em 0.5em; | ||||
|     } | ||||
| 
 | ||||
|     .column { | ||||
|         display: flex; | ||||
|         flex-direction: column; | ||||
|         justify-content: start; | ||||
|     } | ||||
| 
 | ||||
|     .new { | ||||
|         transition: box-shadow 0.2s ease; | ||||
|         background-color: #2973B2; | ||||
|         color: white; | ||||
|         text-decoration: none; | ||||
|         padding: 0.5em 1em; | ||||
|         margin: 1em 0; | ||||
|         width: min-content; | ||||
|         white-space: nowrap; | ||||
|         border-radius: 10px; | ||||
|         box-shadow: 0 0 10px rgba(0, 0, 0, 0.5); | ||||
|     } | ||||
| 
 | ||||
|     .new:hover { | ||||
|         box-shadow: none; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										98
									
								
								framadate/templates/framadate/framadate_detail.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										98
									
								
								framadate/templates/framadate/framadate_detail.html
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,98 @@ | |||
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | ||||
| 
 | ||||
| {% load static %} | ||||
| <link rel="stylesheet" href="{% static 'framadate/style.css' %}"> | ||||
| <link rel="stylesheet" href="{% static 'framadate/framadate_detail.css' %}"> | ||||
| <!-- fonts --> | ||||
| <link rel="preconnect" href="https://fonts.googleapis.com"> | ||||
| <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> | ||||
| <link href="https://fonts.googleapis.com/css2?family=Tomorrow:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap" rel="stylesheet"> | ||||
| <!-- icons --> | ||||
| <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Material+Symbols+Rounded:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200&icon_names=edit" /> | ||||
| 
 | ||||
| <div class="framadate-detail"> | ||||
|     <a href="/framadate" class="back">Back</a> | ||||
|     <h1>{{ framadate.name }} <a href="/framadate/{{ framadate.id }}/delete" class="delete-framadate">Delete</a></h1> | ||||
| 
 | ||||
|     <form class="table" method="post" action="/framadate/{{ framadate.id }}/save-all"> | ||||
|         {% csrf_token %} | ||||
|         <!-- hint --> | ||||
|         <div class="row"> | ||||
|             <div class="item"></div> | ||||
|             {% for mydate in framadate.days_set.all %} | ||||
|             <div class="item">{{ mydate.day|date:"d.m." }} {% if mydate.time %} - {{ mydate.time|time:"H:00" }} {% endif %}</div> | ||||
|             {% endfor %} | ||||
|         </div> | ||||
|         <!-- other users --> | ||||
|         {% for name in framadate.names_set.all %} | ||||
|         <div class="row"> | ||||
|             <div class="item item-with-edit"> | ||||
|                 {{ name.name }} | ||||
|                 <a href="/framadate/{{ framadate.id }}/edit/{{ name.id }}"><span class="material-symbols-rounded">edit</span></a> | ||||
|             </div> | ||||
|             {% for record in name.records_set.all %} | ||||
|             <div class="item {{ record.choiceName }}">{{ record.choiceName }}</div> | ||||
|             {% endfor %} | ||||
|         </div> | ||||
|         {% endfor %} | ||||
|         <!-- hint --> | ||||
|         <div class="row mini-row"> | ||||
|             <div class="item"></div> | ||||
|             {% for mydate in framadate.days_set.all %} | ||||
|             <div class="item">{{ mydate.day|date:"d.m." }} {% if mydate.time %} - {{ mydate.time|time:"H:00" }} {% endif %}</div> | ||||
|             {% endfor %} | ||||
|         </div> | ||||
|         <!-- inputs --> | ||||
|         {% if edditing %} | ||||
|         <div class="row"> | ||||
|             <div class="item center"><input type="text" class="item" name="name" id="name" value="{{ name.name }}"></div> | ||||
|             {% for record in name.records_set.all %} | ||||
|             <div class="item radio-column"> | ||||
|                 <label><input type="radio" name="choice_{{ record.id }}" value="3" {% if record.choice == 3 %} checked {%endif%}>YES</label>  | ||||
|                 <label><input type="radio" name="choice_{{ record.id }}" value="2" {% if record.choice == 2 %} checked {%endif%}>NO</label>  | ||||
|                 <label><input type="radio" name="choice_{{ record.id }}" value="1" {% if record.choice == 1 %} checked {%endif%}>MAYBE</label>  | ||||
|             </div> | ||||
|             {% endfor %} | ||||
|         </div> | ||||
|         {% else %} | ||||
|         <div class="row"> | ||||
|             <div class="item center"><input type="text" class="item" name="name" id="name"></div> | ||||
|             {% for mydate in framadate.days_set.all %} | ||||
|             <div class="item radio-column"> | ||||
|                 <label><input type="radio" name="choice_{{ mydate.id }}" value="3">YES</label> | ||||
|                 <label><input type="radio" name="choice_{{ mydate.id }}" value="2">NO</label>  | ||||
|                 <label><input type="radio" name="choice_{{ mydate.id }}" value="1">MAYBE</label> | ||||
|             </div> | ||||
|             {% endfor %} | ||||
|         </div> | ||||
|         {% endif %} | ||||
|         <!-- save handeling --> | ||||
|         <div class="row"> | ||||
|             <input type="submit" class="submit" value="Save"> | ||||
|             {% if edditing %} | ||||
|             <input name="delete" type="submit" class="submit delete" value="Delete" onclick="return confirmSubmit()">  | ||||
|             <input type="hidden" name="delete_id" value="{{ name.id }}"> | ||||
|             {% endif %} | ||||
|         </div> | ||||
|         <!-- best dates --> | ||||
|         <h3>Best days - {{ max_count }}</h3> | ||||
|         <div class="row"> | ||||
|             {% for mydate in best_dates %} | ||||
|             <div class="item">{{ mydate.day|date:"d.m." }} {% if mydate.time %} - {{ mydate.time|time:"H:00" }} {% endif %}</div> | ||||
|             {% endfor %} | ||||
|         </div> | ||||
|     </form> | ||||
| </div> | ||||
| 
 | ||||
| <script> | ||||
|     function confirmSubmit() { | ||||
|         return confirm("Are you sure you want delete the records?"); | ||||
|     } | ||||
| 
 | ||||
|     // prevent .delete-framadate click event | ||||
|     document.querySelector(".delete-framadate").addEventListener("click", function(event) { | ||||
|         if (!confirm("Are you sure you want delete the framadate?")) { | ||||
|             event.preventDefault(); | ||||
|         } | ||||
|     }); | ||||
| </script> | ||||
							
								
								
									
										31
									
								
								framadate/templates/framadate/framadate_list.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								framadate/templates/framadate/framadate_list.html
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,31 @@ | |||
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | ||||
| 
 | ||||
| {% load static %} | ||||
| <link rel="stylesheet" href="{% static 'framadate/style.css' %}"> | ||||
| <!-- fonts --> | ||||
| <link rel="preconnect" href="https://fonts.googleapis.com"> | ||||
| <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> | ||||
| <link href="https://fonts.googleapis.com/css2?family=Tomorrow:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap" rel="stylesheet"> | ||||
| 
 | ||||
| <div class="column">  | ||||
|     <h1>All current framadates</h1> | ||||
|     <div class="framadates"> | ||||
|         {% for framadate in object_list %} | ||||
|         {% if framadate.isRelevant %} | ||||
|         <a href="framadate/{{ framadate.id }}">{{ framadate.name }}</a> | ||||
|         {% endif %} | ||||
|         {% endfor %} | ||||
|     </div> | ||||
|     <a href="/framadate/new" class="new">Create new</a> | ||||
| </div> | ||||
| 
 | ||||
| <div> | ||||
|     <h1>Past framadates</h1> | ||||
|     <div class="framadates"> | ||||
|         {% for framadate in object_list %} | ||||
|         {% if not framadate.isRelevant %} | ||||
|         <a href="framadate/{{ framadate.id }}">{{ framadate.name }}</a> | ||||
|         {% endif %} | ||||
|         {% endfor %} | ||||
|     </div> | ||||
| </div> | ||||
							
								
								
									
										82
									
								
								framadate/templates/framadate/new_framadate.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										82
									
								
								framadate/templates/framadate/new_framadate.html
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,82 @@ | |||
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | ||||
| 
 | ||||
| {% load static %} | ||||
| <link rel="stylesheet" href="{% static 'framadate/style.css' %}"> | ||||
| <link rel="stylesheet" href="{% static 'framadate/new_framadate.css' %}"> | ||||
| <!-- fonts --> | ||||
| <link rel="preconnect" href="https://fonts.googleapis.com"> | ||||
| <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> | ||||
| <link href="https://fonts.googleapis.com/css2?family=Tomorrow:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap" rel="stylesheet"> | ||||
| 
 | ||||
| <a href="/framadate" class="back">Back</a> | ||||
| <h1>New framadate</h1> | ||||
| 
 | ||||
| <form action="/framadate/new" method="post"> | ||||
|     {% csrf_token %} | ||||
|     <label class="label-name">name: <input type="text" required name="name"></label> | ||||
| 
 | ||||
|     <div class="dates"> | ||||
|         <!-- <div id="date_1" class="date"> | ||||
|             <label>date: <input type="date" name="" id="" required></label> | ||||
|             <label>time (optional, if empty -> entire day ): <input type="time" name="" id="" step="3600000"></label> | ||||
|             <button type="button" onclick="removeDate(1)">remove</button> | ||||
|         </div> --> | ||||
|     </div> | ||||
|     <!-- controls --> | ||||
|     <div class="controls"> | ||||
|         <button type="button" class="add-button" onclick="addDate()">Add date</button> | ||||
|         <div class="span-controls"> | ||||
|             <label>from: <input id="from" type="date"></label> | ||||
|             <label>to: <input id="to" type="date"></label> | ||||
|             <button type="button" onclick="addSpan()" class="add-button">Add span</button> | ||||
|         </div> | ||||
|     </div> | ||||
|     <input type="submit" value="Create" class="add-button create"> | ||||
| </form> | ||||
| 
 | ||||
| 
 | ||||
| <script> | ||||
|     numberOfDates = 1; | ||||
| 
 | ||||
|     function addDate() { | ||||
|         let newDate = document.createElement("div"); | ||||
|         newDate.classList.add("date"); | ||||
|         numberOfDates++; | ||||
|         newDate.id = "date_" + numberOfDates; | ||||
|         // step does not work in safari... | ||||
|         newDate.innerHTML = ` | ||||
|             <label>date: <input type="date" name="date" required></label> | ||||
|             <label>time (optional, if empty -> entire day ): <input type="time" name="time" step="3600"></label> | ||||
|             <button type="button" onclick="removeDate(`+numberOfDates+`)">remove</button> | ||||
|         `; | ||||
|         dates = document.querySelector(".dates"); | ||||
|         dates.appendChild(newDate); | ||||
|     } | ||||
| 
 | ||||
|     function removeDate(id_to_remove) { | ||||
|         document.getElementById("date_" + id_to_remove).remove(); | ||||
|     } | ||||
| 
 | ||||
|     let from = document.getElementById("from"); | ||||
|     let to = document.getElementById("to"); | ||||
|     function addSpan() { | ||||
|         let fromDate = new Date(from.value); | ||||
|         let toDate = new Date(to.value); | ||||
|         // print date of every date in range | ||||
|         while (fromDate <= toDate) { | ||||
|             let newDate = document.createElement("div"); | ||||
|             newDate.classList.add("date"); | ||||
|             numberOfDates++; | ||||
|             newDate.id = "date_" + numberOfDates; | ||||
|             newDate.innerHTML = ` | ||||
|                 <label>date: <input type="date" name="date" required value="`+fromDate.toISOString().split("T")[0]+`"></label> | ||||
|                 <label>time (optional, if empty -> entire day ): <input type="time" name="time" step="3600"></label> | ||||
|                 <button type="button" onclick="removeDate(`+numberOfDates+`)">remove</button> | ||||
|             `; | ||||
|             dates = document.querySelector(".dates"); | ||||
|             dates.appendChild(newDate); | ||||
|             fromDate.setDate(fromDate.getDate() + 1); | ||||
|         } | ||||
| 
 | ||||
|     } | ||||
| </script> | ||||
							
								
								
									
										3
									
								
								framadate/tests.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								framadate/tests.py
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,3 @@ | |||
| from django.test import TestCase | ||||
| 
 | ||||
| # Create your tests here. | ||||
							
								
								
									
										11
									
								
								framadate/urls.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								framadate/urls.py
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,11 @@ | |||
| from django.urls import path | ||||
| from . import views | ||||
| 
 | ||||
| urlpatterns = [ | ||||
|     path("framadate", views.FramadateListView.as_view(), name="framadate_list"), | ||||
|     path("framadate/new", views.NewFramadate.as_view(), name="new_framadate"), | ||||
|     path("framadate/<int:pk>", views.FramadateDetail.as_view(), name="framadate_detail"), | ||||
|     path("framadate/<int:pk>/edit/<int:name_id>", views.FramadateDetailEdit.as_view(), name="framadate_detail_edit"), | ||||
|     path("framadate/<int:pk>/save-all", views.save_all, name="save_all"), | ||||
|     path("framadate/<int:pk>/delete", views.delete_framadate, name="delete"), | ||||
| ] | ||||
							
								
								
									
										93
									
								
								framadate/views.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										93
									
								
								framadate/views.py
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,93 @@ | |||
| from django.http import HttpResponse, HttpResponseRedirect | ||||
| from django.urls import reverse | ||||
| from django.views.generic.list import ListView | ||||
| from django.views.generic import TemplateView | ||||
| from .models import * | ||||
| 
 | ||||
| class FramadateListView(ListView): | ||||
|     model = Framadate | ||||
|     template_name = 'framadate/framadate_list.html' | ||||
| 
 | ||||
| class NewFramadate(TemplateView): | ||||
|     template_name = 'framadate/new_framadate.html' | ||||
| 
 | ||||
|     def post(self, request, *args, **kwargs): | ||||
|         new_framadate = Framadate.objects.create(name=request.POST['name']) | ||||
|         dates = request.POST.getlist('date') | ||||
|         times = request.POST.getlist('time') | ||||
|         for i in range(len(dates)): | ||||
|             FramadateDate.objects.create(framadate=new_framadate, day=dates[i], time=(times[i] if times[i] else None)) | ||||
|         return HttpResponseRedirect("/framadate/" + str(new_framadate.id)) | ||||
| 
 | ||||
| class FramadateDetail(TemplateView): | ||||
|     template_name = 'framadate/framadate_detail.html' | ||||
| 
 | ||||
|     def get_context_data(self, **kwargs): | ||||
|         context = super().get_context_data(**kwargs) | ||||
|         addContextForFramadate(context, **kwargs) | ||||
|         return context | ||||
|      | ||||
| class FramadateDetailEdit(TemplateView): | ||||
|     template_name = 'framadate/framadate_detail.html' | ||||
| 
 | ||||
|     def get_context_data(self, **kwargs): | ||||
|         context = super().get_context_data(**kwargs) | ||||
|         addContextForFramadate(context, **kwargs) | ||||
|         # data to edit | ||||
|         context["edditing"] = True | ||||
|         context["name"] = NameInFramadate.objects.get(pk=kwargs['name_id']) | ||||
|         return context | ||||
|      | ||||
| def save_all(request, pk): | ||||
|     if request.method != "POST": | ||||
|         return HttpResponse("Not a POST request") | ||||
|     if request.POST.get("name") == "": | ||||
|         return HttpResponseRedirect(reverse('framadate_detail', args=[pk])) | ||||
|     framadate = Framadate.objects.get(pk=pk) | ||||
|     # deleting | ||||
|     if request.POST.get("delete"): | ||||
|         try: | ||||
|             NameInFramadate.objects.get(pk=request.POST['delete_id']).delete() | ||||
|         except: | ||||
|             return HttpResponse("Ups something went wrong, can't delete") | ||||
|         return HttpResponseRedirect(reverse('framadate_detail', args=[pk])) | ||||
|     # new | ||||
|     if not request.POST.get("delete_id"): | ||||
|         new_name = NameInFramadate.objects.create(framadate=framadate, name=request.POST['name']) | ||||
|         for day in framadate.days_set.all(): | ||||
|             Record.objects.create(nameInFramadate=new_name, framadateDate=day, choice=(request.POST["choice_" + str(day.id)] if request.POST.get("choice_" + str(day.id)) else 0)) | ||||
|     else: # edditing | ||||
|         name_id = request.POST['delete_id'] | ||||
|         name = NameInFramadate.objects.get(pk=name_id) | ||||
|         name.name = request.POST['name'] | ||||
|         name.save() | ||||
|         for record in name.records_set.all(): | ||||
| 
 | ||||
|             record.choice = (request.POST["choice_" + str(record.id)] if request.POST.get("choice_" + str(record.id)) else 0) | ||||
|             record.save() | ||||
|     return HttpResponseRedirect(reverse('framadate_detail', args=[pk])) | ||||
| 
 | ||||
| def delete_framadate(request, pk): | ||||
|     framadate = Framadate.objects.get(pk=pk) | ||||
|     framadate.delete() | ||||
|     return HttpResponseRedirect("/framadate") | ||||
| 
 | ||||
| def addContextForFramadate(context, **kwargs): | ||||
|     context['framadate'] = Framadate.objects.get(pk=kwargs['pk']) | ||||
|     context['choices'] = Choice.choices | ||||
|     context['best_dates'] = [] | ||||
|     best_dates = [] | ||||
|     best = -1 | ||||
|     for day in context['framadate'].days_set.all(): # kinda slow? | ||||
|         count = 0 | ||||
|         for record in day.records_set.all(): | ||||
|             if record.choice == 3: | ||||
|                 count += 1 | ||||
|         if count > best: | ||||
|             best_dates = [day] | ||||
|             best = count | ||||
|         elif count == best: | ||||
|             best_dates.append(day) | ||||
|     context['best_dates'] = best_dates | ||||
|     context['max_count'] = best | ||||
|      | ||||
|  | @ -148,6 +148,7 @@ INSTALLED_APPS = ( | |||
| 	'vyroci', | ||||
| 	'sifrovacka', | ||||
| 	'novinky', | ||||
| 	'framadate', | ||||
| 
 | ||||
| 	# Admin upravy: | ||||
| 
 | ||||
|  |  | |||
|  | @ -62,6 +62,9 @@ urlpatterns = [ | |||
| 
 | ||||
| 	# Miniapka na šifrovačku | ||||
| 	path('sifrovacka/', include('sifrovacka.urls')), | ||||
| 
 | ||||
| 	# Framadate | ||||
| 	path('', include('framadate.urls')), | ||||
| ] | ||||
| 
 | ||||
| # This is only needed when using runserver. | ||||
|  |  | |||
|  | @ -13,3 +13,4 @@ from soustredeni.models import * | |||
| from treenode.models import * | ||||
| from tvorba.models import * | ||||
| from various.models import * | ||||
| from framadate.models import * | ||||
|  |  | |||
		Loading…
	
		Reference in a new issue