The AdminTimeWidget rendered in admin for a DateTimeField displays an icon of a clock and when you click you have the choice between: "Now Midnight 6:00 Noon".
How can I change these choices to "16h 17h 18h"?
Chris has a great answer. As an alternative you could do this using just javascript. Place the following javascript on the pages where you want the different time options.
DateTimeShortcuts.overrideTimeOptions = function () {
// Find the first time element
timeElement = django.jQuery("ul.timelist li").eq(0).clone();
originalHref = timeElement.find('a').attr('href');
// remove all existing time elements
django.jQuery("ul.timelist li").remove();
// add new time elements representing those you want
var i=0;
for (i=0;i<=23;i++) {
// use a regular expression to update the link
newHref = originalHref.replace(/Date\([^\)]*\)/g, "Date(1970,1,1," + i + ",0,0,0)");
// update the text for the element
timeElement.find('a').attr('href', newHref).text(i+"h");
// Add the new element into the document
django.jQuery("ul.timelist").append(timeElement.clone());
}
}
addEvent(window, 'load', DateTimeShortcuts.overrideTimeOptions);
Subclass AdminTimeWidget to include a modified DateTimeShortcuts.js (get to that in a sec), then subclass AdminSplitDateTime to include your subclassed MyAdminTimeWidget instead of the default Django one:
from django.contrib.admin.widgets import AdminTimeWidget
from django.conf import settings
class MyAdminTimeWidget(AdminTimeWidget):
class Media:
js = (settings.ADMIN_MEDIA_PREFIX + "js/calendar.js",
settings.MEDIA_URL + "js/admin/DateTimeShortcuts.js")
class MyAdminSplitDateTime(AdminSplitDateTime):
def __init__(self, attrs=None):
widgets = [AdminDateWidget, MyAdminTimeWidget]
forms.MultiWidget.__init__(self, widgets, attrs)
The secret sauce is in django/contrib/admin/media/js/admin/DateTimeShortcuts.js. This is what creates the list you want to modify. Copy this file and paste it into your project's site_media/js/admin directory. The relevant code you need to modify is on lines 85-88:
quickElement("a", quickElement("li", time_list, ""), gettext("Now"), "href", "javascript:DateTimeShortcuts.handleClockQuicklink(" + num + ", new Date().strftime('" + time_format + "'));");
quickElement("a", quickElement("li", time_list, ""), gettext("Midnight"), "href", "javascript:DateTimeShortcuts.handleClockQuicklink(" + num + ", new Date(1970,1,1,0,0,0,0).strftime('" + time_format + "'));");
quickElement("a", quickElement("li", time_list, ""), gettext("6 a.m."), "href", "javascript:DateTimeShortcuts.handleClockQuicklink(" + num + ", new Date(1970,1,1,6,0,0,0).strftime('" + time_format + "'));");
quickElement("a", quickElement("li", time_list, ""), gettext("Noon"), "href", "javascript:DateTimeShortcuts.handleClockQuicklink(" + num + ", new Date(1970,1,1,12,0,0,0).strftime('" + time_format + "'));");
Simply add to/delete from/modify that bit of javascript to your heart's content.
Finally, attach your new widget to any DateTimeFields you like. Your best bet for that will probably be the formfield_overrides attribute on ModelAdmin:
class MyModelAdmin(admin.ModelAdmin):
formfield_overrides = {
models.DateTimeField: {'widget': MyAdminSplitDateTime},
}
I tried using this method and found the above javascript didn't work when multiple datetime's were present on the form.
here is what I did.
In my ModelAdmin section i added:
class Media:
js = ('js/clock_time_selections.js',)
then in the js file:
$('document').ready(function () {
DateTimeShortcuts.overrideTimeOptions = function () {
var clockCount = 0;
console.log('ready');
$('ul.timelist').each(function () {
var $this = $(this);
var originalHref = $this.find('a').attr('href');
console.log(originalHref);
$this.find('li').remove();
for (i=8; i <= 20; i++) {
var newLink = '<li><a href="javascript:DateTimeShortcuts.handleClockQuicklink('+ clockCount + ', ' + i
+ ');"> ' + i + ':00h</a></li>';
$this.append(newLink);
}
//console.log($this.html());
clockCount++;
});
};
addEvent(window, 'load', DateTimeShortcuts.overrideTimeOptions);
});
Note: i had to put inside a document.ready because i found that i couldn't control where the script was included in the page (seems to have be loaded before the default calendar js files).
There's better solution. After reading DateTimeShortcuts.js the can be simplified to:
(function ($) {
$(document).ready(function () {
DateTimeShortcuts.clockHours.default_ = [];
for (let hour = 8; hour <= 20; hour++) {
let verbose_name = new Date(1970, 1, 1, hour, 0, 0).strftime('%H:%M');
DateTimeShortcuts.clockHours.default_.push([verbose_name, hour])
}
});
})(django.jQuery);
Then add this code to the javascript file in 'static//time-shortcuts.js' and add Meta to your admin model:
from django.contrib import admin
from .models import MyModel
#admin.register(MyModel)
class MyModelAdmin(admin.ModelAdmin):
class Media:
js = [
'<myapp>/time-shortcuts.js',
]
I went with a much simpler approach and it worked for me. I simply added choices to my model using the following code:
class Class(Model):
program = ForeignKey('Program')
time_of_the_day = TimeField(choices=(
(datetime.datetime.strptime('7:00 am', "%I:%M %p").time(), '7:00 am'),
(datetime.datetime.strptime('8:00 am', "%I:%M %p").time(), '8:00 am'),
(datetime.datetime.strptime('9:00 am', "%I:%M %p").time(), '9:00 am'),
(datetime.datetime.strptime('6:00 pm', "%I:%M %p").time(), '6:00 pm'),
(datetime.datetime.strptime('7:00 pm', "%I:%M %p").time(), '7:00 pm'),
(datetime.datetime.strptime('8:00 pm', "%I:%M %p").time(), '8:00 pm'),
(datetime.datetime.strptime('9:00 pm', "%I:%M %p").time(), '9:00 pm'),
))
Hope this helps
Overriding JS by DateTimeShortcuts.overrideTimeOptions function works only with one form
( bug: the change of time in child model affects parent model, so you can't change timefield in child model form by this widget)
If you want use custom time options with inlines:
in /static/admin/js/admin/DateTimeShortcuts.js
replace:
quickElement("a", quickElement("li", time_list, ""), gettext("Now"), "href", "javascript:DateTimeShortcuts.handleClockQuicklink(" + num + ", new Date().strftime('" + time_format + "'));");
quickElement("a", quickElement("li", time_list, ""), gettext("Midnight"), "href", "javascript:DateTimeShortcuts.handleClockQuicklink(" + num + ", new Date(1970,1,1,0,0,0,0).strftime('" + time_format + "'));");
quickElement("a", quickElement("li", time_list, ""), gettext("6 a.m."), "href", "javascript:DateTimeShortcuts.handleClockQuicklink(" + num + ", new Date(1970,1,1,6,0,0,0).strftime('" + time_format + "'));");
quickElement("a", quickElement("li", time_list, ""), gettext("Noon"), "href", "javascript:DateTimeShortcuts.handleClockQuicklink(" + num + ", new Date(1970,1,1,12,0,0,0).strftime('" + time_format + "'));");
by:
for(j=6;j<=23;j++){
quickElement("a", quickElement("li", time_list, ""), j+":00", "href", "javascript:DateTimeShortcuts.handleClockQuicklink(" + num + ", new Date(1970,1,1," + j + ",0,0,0).strftime('" + time_format + "'));");
}
Expanding on #Bit68's answer, assuming other folks might want to create lists of regularly spaced times more than once, I created a helper function to build a choices tuple. (I'm adding a new answer because this length of code is too hard to follow in a comment.) This works in Django 2.2.
Note that this creates a dropdown list of options, it doesn't add options to the default admin date/time widget as the javascript methods do.
import datetime
def get_time_choices(start_time=datetime.time(9,0,0), end_time=datetime.time(17,0,0), delta=datetime.timedelta(minutes=15)):
'''
Builds a choices tuple of (time object, time string) tuples
starting at the start time specified and ending at or before
the end time specified in increments of size delta.
The default is to return a choices tuple for
9am to 5pm in 15-minute increments.
'''
time_choices = ()
time = start_time
while time <= end_time:
time_choices += ((time, time.strftime("%I:%M %p")),)
# This complicated line is because you can't add
# a timedelta object to a time object.
time = (datetime.datetime.combine(datetime.date.today(), time) + delta).time()
return time_choices
Then time_of_the_day = models.TimeField(choices=get_time_choices())
Related
I am working on an application with Django. There in this application, I am first using Django to create a database with points and extract a JSON file (It is called "markers.json"). Then, using this JSON file, I am creating markers on a map with Leaflet. When I finished entering all the points to the database they will be around 5000 thousand. So, I decided that it is a good idea to be able to search this markers with an input tag and a search button. I enter the "site_name" as input and when I click the "search" button the related marker should popup. However, always the same marker pops up and I don't know where I am doing wrong.
Could you please help me on that?
HTML PART
<input type="text" id="mast_find" name="mastName" placeholder="Search or masts...">
<button type="submit" id="mast_button">Search</button>
JAVASCRIPT PART
var streets = L.tileLayer( 'http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '© OpenStreetMap',
subdomains: ['a', 'b', 'c']
}),
esri = L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', {
attribution: 'Tiles © Esri — Source: Esri, i-cubed, USDA, USGS, AEX, GeoEye, Getmapping, Aerogrid, IGN, IGP, UPR-EGP, and the GIS User Community'
}),
topo = L.tileLayer('https://{s}.tile.opentopomap.org/{z}/{x}/{y}.png', {
maxZoom: 17,
attribution: 'Map data: © OpenStreetMap, SRTM | Map style: © OpenTopoMap (CC-BY-SA)'
});
var map = L.map( 'map', {
center: [20.0, 5.0],
minZoom: 2,
zoom: 2,
layers: [streets, esri, topo]
})
var baseMaps = {
"Streets": streets,
"Esri": esri,
"Topo": topo
};
$('.leaflet-control-attribution').hide()
L.control.scale().addTo(map);
L.control.layers(baseMaps).addTo(map);
var myURL = jQuery( 'script[src$="leaf.js"]' ).attr( 'src' ).replace( 'leaf.js', '' )
var myIcon = L.icon({
iconUrl: myURL + '/images/pin24.png',
iconRetinaUrl: myURL + '/images/pin48.png',
iconSize: [29, 24],
iconAnchor: [9, 21],
popupAnchor: [0, -14]
})
for ( var i=0; i < markers.length; ++i )
{
var deneme = [];
var meleme = L.marker( [markers[i].fields.latitude, markers[i].fields.longitude], {icon: myIcon} )
.bindPopup( "<b>" + "Mast name: " + "</b>" + markers[i].fields.site_name + "<b>" + "<br>" + "A: " + "</b>" + markers[i].fields.a_measured_height_lt + "<br>" + "<b>" + "k: " + "</b>" + markers[i].fields.k_measured_height_lt )
.addTo( map );
deneme.push(meleme);
document.getElementById("mast_button").onclick = mastFunct;
function mastFunct(){
var data = document.getElementById("mast_find");
for (var i=0; i < markers.length; ++i ){
var markerID = markers[i].fields.site_name;
if (markerID = data.value){
deneme[i].openPopup()
}
}
};
if (markerID = data.value){
should be
if (markerID == data.value){
the only issue that i see is this with the if (markerID = data.value){.
But you can try this alternative:
instead your for-loop:
map.eachLayer(function(marker){
if(marker.options){
var markerID = marker.options.site_name;
if (markerID == data.value){
marker.openPopup();
}
}
});
and add this to your marker creation:
L.marker([51.493782, -0.089951],{icon: myIcon, site_name: 'test'}).addTo(map)
I recently upgraded to Django 2.2.2 and Python 3.6.8, and my filter_horizontal feature in Django admin has disappeared.
I tried viewing my admin in Chrome incognito mode, as some answers suggest, and I also tried changing verbose_name strings to unicode. However, none of these worked.
Here is an example of a model for which I am attempting to show filter_horizontal. This worked on my app prior to the upgrades.
admin.py
class ResearchAdmin(admin.ModelAdmin):
filter_horizontal = ('criteria', 'state')
def save_model(self, request, obj, form, change):
obj.save()
# Update cache for tasks containing the saved goal
tasks = Task.objects.filter(research__id=obj.pk).values_list('pk', flat=True)
for t in tasks:
cache.delete_many(['task%s' % t, 'task%s_research' % t])
models.py
"""
Clinical Research model
"""
def __str__(self):
return "%s" % (self.name)
name = models.CharField(max_length=50, null=True, blank=True)
type = models.CharField(max_length=50, null=True, blank=True)
cta = models.CharField(max_length=50, null=True, blank=True)
description = models.TextField(null=True, blank=True)
picture = models.ImageField(upload_to='images/%Y/%m/%d', null=True, blank=True, help_text="Upload portrait image for modal study description")
layout = models.CharField(max_length=1, choices=LAYOUT_TYPE, null=False, blank=False, default='r')
criteria = models.ManyToManyField('Answer', blank=True, db_index=True, help_text="Answers required for qualified patients")
required_contact = models.ManyToManyField('ContactField', blank=True, db_index=True, help_text="Contact info for patient to enter")
email = models.EmailField(null=True, blank=True, help_text="Sponsor email for notifying of screened patients")
link = models.URLField(null=True, blank=True)
state = models.ManyToManyField('State', blank=True, help_text="Qualifying states")
lat = models.CharField(max_length=60, null=True, blank=True)
lng = models.CharField(max_length=60, null=True, blank=True)
distance = models.PositiveIntegerField(null=True, blank=True, help_text="Maximum distance from user in miles to show")
class Meta:
verbose_name = u"Research"
verbose_name_plural = u"Research"
There are no error messages, but the filter_horizontal frontend doesn't show up in admin for the criteria and state fields.
##collectstatic##
As #iain-shelvington suggested, this issue may be due to some kind of interference with the cached front-end code required to display the filter_horizontal format. I have tried running in Google Incognito mode, clearing catch, and running collectstatic --clear, and none of these work. Moreover, there aren't any differences between the admin static files pre- and post- upgrade.
#SylvainBiehler pointed out that django_gulp may be overriding collectstatic. I disabled django_gulp and ran ./manage.py collectstatic --clear all admin files are now updated post Django upgrade.
##Comparing admin files pre- and post- Django upgrade##
I was able to spin up a version of my app pre-Django upgrade, and the filter_horizontal capability works in the older version. There are some differences in the construction of the Criteria field from Chrome console:
Old Version (works)
Select element prior to choices:
<select multiple="multiple" class="selectfilter" id="id_criteria" name="criteria">
Javascript after choices:
<script type="text/javascript">addEvent(window, "load", function(e) {SelectFilter.init("id_criteria", "criteria", 0, "https://xxxxxxx/static/admin/"); });</script>
New Version (broken)
Slightly different select element. No javascript after choices:
<select name="criteria" id="id_criteria" multiple class="selectfilter" data-field-name="criteria" data-is-stacked="0">
This seems to be causing the issue, but I have no idea how to fix it. Any ideas?
##Analyzing admin JS in console##
SelectBox.js loads in the console and declares var = SelectBox = {... which is never called.
SelectFilter.js also loads but the function is never called:
/*
SelectFilter2 - Turns a multiple-select box into a filter interface.
Requires core.js, SelectBox.js and addevent.js.
*/
(function($) {
function findForm(node) {
// returns the node of the form containing the given node
if (node.tagName.toLowerCase() != 'form') {
return findForm(node.parentNode);
}
return node;
}
window.SelectFilter = {
init: function(field_id, field_name, is_stacked, admin_static_prefix) {
if (field_id.match(/__prefix__/)){
// Don't intialize on empty forms.
return;
}
var from_box = document.getElementById(field_id);
from_box.id += '_from'; // change its ID
from_box.className = 'filtered';
var ps = from_box.parentNode.getElementsByTagName('p');
for (var i=0; i<ps.length; i++) {
if (ps[i].className.indexOf("info") != -1) {
// Remove <p class="info">, because it just gets in the way.
from_box.parentNode.removeChild(ps[i]);
} else if (ps[i].className.indexOf("help") != -1) {
// Move help text up to the top so it isn't below the select
// boxes or wrapped off on the side to the right of the add
// button:
from_box.parentNode.insertBefore(ps[i], from_box.parentNode.firstChild);
}
}
// <div class="selector"> or <div class="selector stacked">
var selector_div = quickElement('div', from_box.parentNode);
selector_div.className = is_stacked ? 'selector stacked' : 'selector';
// <div class="selector-available">
var selector_available = quickElement('div', selector_div, '');
selector_available.className = 'selector-available';
var title_available = quickElement('h2', selector_available, interpolate(gettext('Available %s') + ' ', [field_name]));
quickElement('img', title_available, '', 'src', admin_static_prefix + 'img/icon-unknown.gif', 'width', '10', 'height', '10', 'class', 'help help-tooltip', 'title', interpolate(gettext('This is the list of available %s. You may choose some by selecting them in the box below and then clicking the "Choose" arrow between the two boxes.'), [field_name]));
var filter_p = quickElement('p', selector_available, '', 'id', field_id + '_filter');
filter_p.className = 'selector-filter';
var search_filter_label = quickElement('label', filter_p, '', 'for', field_id + "_input");
var search_selector_img = quickElement('img', search_filter_label, '', 'src', admin_static_prefix + 'img/selector-search.gif', 'class', 'help-tooltip', 'alt', '', 'title', interpolate(gettext("Type into this box to filter down the list of available %s."), [field_name]));
filter_p.appendChild(document.createTextNode(' '));
var filter_input = quickElement('input', filter_p, '', 'type', 'text', 'placeholder', gettext("Filter"));
filter_input.id = field_id + '_input';
selector_available.appendChild(from_box);
var choose_all = quickElement('a', selector_available, gettext('Choose all'), 'title', interpolate(gettext('Click to choose all %s at once.'), [field_name]), 'href', 'javascript: (function(){ SelectBox.move_all("' + field_id + '_from", "' + field_id + '_to"); SelectFilter.refresh_icons("' + field_id + '");})()', 'id', field_id + '_add_all_link');
choose_all.className = 'selector-chooseall';
// <ul class="selector-chooser">
var selector_chooser = quickElement('ul', selector_div, '');
selector_chooser.className = 'selector-chooser';
var add_link = quickElement('a', quickElement('li', selector_chooser, ''), gettext('Choose'), 'title', gettext('Choose'), 'href', 'javascript: (function(){ SelectBox.move("' + field_id + '_from","' + field_id + '_to"); SelectFilter.refresh_icons("' + field_id + '");})()', 'id', field_id + '_add_link');
add_link.className = 'selector-add';
var remove_link = quickElement('a', quickElement('li', selector_chooser, ''), gettext('Remove'), 'title', gettext('Remove'), 'href', 'javascript: (function(){ SelectBox.move("' + field_id + '_to","' + field_id + '_from"); SelectFilter.refresh_icons("' + field_id + '");})()', 'id', field_id + '_remove_link');
remove_link.className = 'selector-remove';
// <div class="selector-chosen">
var selector_chosen = quickElement('div', selector_div, '');
selector_chosen.className = 'selector-chosen';
var title_chosen = quickElement('h2', selector_chosen, interpolate(gettext('Chosen %s') + ' ', [field_name]));
quickElement('img', title_chosen, '', 'src', admin_static_prefix + 'img/icon-unknown.gif', 'width', '10', 'height', '10', 'class', 'help help-tooltip', 'title', interpolate(gettext('This is the list of chosen %s. You may remove some by selecting them in the box below and then clicking the "Remove" arrow between the two boxes.'), [field_name]));
var to_box = quickElement('select', selector_chosen, '', 'id', field_id + '_to', 'multiple', 'multiple', 'size', from_box.size, 'name', from_box.getAttribute('name'));
to_box.className = 'filtered';
var clear_all = quickElement('a', selector_chosen, gettext('Remove all'), 'title', interpolate(gettext('Click to remove all chosen %s at once.'), [field_name]), 'href', 'javascript: (function() { SelectBox.move_all("' + field_id + '_to", "' + field_id + '_from"); SelectFilter.refresh_icons("' + field_id + '");})()', 'id', field_id + '_remove_all_link');
clear_all.className = 'selector-clearall';
from_box.setAttribute('name', from_box.getAttribute('name') + '_old');
// Set up the JavaScript event handlers for the select box filter interface
addEvent(filter_input, 'keyup', function(e) { SelectFilter.filter_key_up(e, field_id); });
addEvent(filter_input, 'keydown', function(e) { SelectFilter.filter_key_down(e, field_id); });
addEvent(from_box, 'change', function(e) { SelectFilter.refresh_icons(field_id) });
addEvent(to_box, 'change', function(e) { SelectFilter.refresh_icons(field_id) });
addEvent(from_box, 'dblclick', function() { SelectBox.move(field_id + '_from', field_id + '_to'); SelectFilter.refresh_icons(field_id); });
addEvent(to_box, 'dblclick', function() { SelectBox.move(field_id + '_to', field_id + '_from'); SelectFilter.refresh_icons(field_id); });
addEvent(findForm(from_box), 'submit', function() { SelectBox.select_all(field_id + '_to'); });
SelectBox.init(field_id + '_from');
SelectBox.init(field_id + '_to');
// Move selected from_box options to to_box
SelectBox.move(field_id + '_from', field_id + '_to');
if (!is_stacked) {
// In horizontal mode, give the same height to the two boxes.
var j_from_box = $(from_box);
var j_to_box = $(to_box);
var resize_filters = function() { j_to_box.height($(filter_p).outerHeight() + j_from_box.outerHeight()); }
if (j_from_box.outerHeight() > 0) {
resize_filters(); // This fieldset is already open. Resize now.
} else {
// This fieldset is probably collapsed. Wait for its 'show' event.
j_to_box.closest('fieldset').one('show.fieldset', resize_filters);
}
}
// Initial icon refresh
SelectFilter.refresh_icons(field_id);
},
refresh_icons: function(field_id) {
var from = $('#' + field_id + '_from');
var to = $('#' + field_id + '_to');
var is_from_selected = from.find('option:selected').length > 0;
var is_to_selected = to.find('option:selected').length > 0;
// Active if at least one item is selected
$('#' + field_id + '_add_link').toggleClass('active', is_from_selected);
$('#' + field_id + '_remove_link').toggleClass('active', is_to_selected);
// Active if the corresponding box isn't empty
$('#' + field_id + '_add_all_link').toggleClass('active', from.find('option').length > 0);
$('#' + field_id + '_remove_all_link').toggleClass('active', to.find('option').length > 0);
},
filter_key_up: function(event, field_id) {
var from = document.getElementById(field_id + '_from');
// don't submit form if user pressed Enter
if ((event.which && event.which == 13) || (event.keyCode && event.keyCode == 13)) {
from.selectedIndex = 0;
SelectBox.move(field_id + '_from', field_id + '_to');
from.selectedIndex = 0;
return false;
}
var temp = from.selectedIndex;
SelectBox.filter(field_id + '_from', document.getElementById(field_id + '_input').value);
from.selectedIndex = temp;
return true;
},
filter_key_down: function(event, field_id) {
var from = document.getElementById(field_id + '_from');
// right arrow -- move across
if ((event.which && event.which == 39) || (event.keyCode && event.keyCode == 39)) {
var old_index = from.selectedIndex;
SelectBox.move(field_id + '_from', field_id + '_to');
from.selectedIndex = (old_index == from.length) ? from.length - 1 : old_index;
return false;
}
// down arrow -- wrap around
if ((event.which && event.which == 40) || (event.keyCode && event.keyCode == 40)) {
from.selectedIndex = (from.length == from.selectedIndex + 1) ? 0 : from.selectedIndex + 1;
}
// up arrow -- wrap around
if ((event.which && event.which == 38) || (event.keyCode && event.keyCode == 38)) {
from.selectedIndex = (from.selectedIndex == 0) ? from.length - 1 : from.selectedIndex - 1;
}
return true;
}
}
})(django.jQuery);
##INSTALLED APPS##
########## APP CONFIGURATION
INSTALLED_APPS = DJANGO_APPS + LOCAL_APPS
DJANGO_APPS = (
'django_gulp',
# Default Django apps:
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.sites',
'django.contrib.messages',
'django.contrib.staticfiles',
# Useful template tags:
# 'django.contrib.humanize',
# Python-Social-Auth
'social_django',
# Admin panel and documentation:
'django.contrib.admin',
'django.core.management',
# 'django.contrib.admindocs',
# for task queue
# For django-storages static store on AWS S3
'storages',
# Other django apps
'rest_framework',
)
# Apps specific for this project go here.
LOCAL_APPS = (
'base',
'myproject',
'users',
'campaigns',
)
I copied the static/admin files from Django.contrib using the following command in terminal:
cp -a /Users/username/.virtualenvs/rs/lib/python3.9/site-packages/django/contrib/admin/. /Users/username/Documents/myproject/static/
I then ran collectstatic to upload the files to S3, where they are stored for production. This actually worked, but seems a bit hacky. I must not be doing something right for these files to not update upon Django upgrade.
How can I programmatically download the PDF monthly invoice the accounting department ask me every month?
I can get them from AWS Console (eg. https://console.aws.amazon.com/billing/home?region=eu-west-3#/bills?year=2019&month=3)
Where there is a link to the invoice.
The moment I click to download the invoice, I can see HTTP requests to the following URL:
https://console.aws.amazon.com/billing/rest/v1.0/bill/invoice/generate?generatenew=true&invoiceGroupId=_SOME_ID_&invoicenumber=_SOME_ID_
Then a final request to the URL that actually serves the PDF file:
https://console.aws.amazon.com/billing/rest/v1.0/bill/invoice/download?invoiceGroupId=_SOME_ID_&invoicenumber=_SOME_ID_
I cannot find documentation on the AWS API to fetch such invoice document (there is some for billing reports and other stuff, but none for the "official" document) so I start to ask myself if it is even available?
Before going scraping the AWS Console (via Scrapy, Selenium, Puppeteer) I ask the community.
NB: I know AWS can send the invoice PDF via e-mail but I rather fetch it directly from AWS instead of fetching from an IMAP/POP e-mail server.
You can use aws cli or aws sdk to get the data in json format. And then convert the json into pdf (not covered in this answer).
AWS cli
aws cli provides get-cost-and-usage command. By fiddling with parameters you can get the output that matches the one that is produced by billing invoice.
Example usage of this command:
aws ce get-cost-and-usage \
--time-period Start=2019-03-01,End=2019-04-01 \
--granularity MONTHLY \
--metrics "BlendedCost" "UnblendedCost" "UsageQuantity" \
--group-by Type=DIMENSION,Key=SERVICE
Which produces the following output
{
"GroupDefinitions": [
{
"Type": "DIMENSION",
"Key": "SERVICE"
}
],
"ResultsByTime": [
{
"TimePeriod": {
"Start": "2019-03-01",
"End": "2019-04-01"
},
"Total": {},
"Groups": [
{
"Keys": [
"AWS Budgets"
],
"Metrics": {
"BlendedCost": {
"Amount": "3.0392156805",
"Unit": "USD"
},
"UnblendedCost": {
"Amount": "3",
"Unit": "USD"
},
"UsageQuantity": {
"Amount": "155",
"Unit": "N/A"
}
}
},
{
"Keys": [
"AWS CloudTrail"
],
"Metrics": {
"BlendedCost": {
"Amount": "0",
"Unit": "USD"
},
"UnblendedCost": {
"Amount": "0",
"Unit": "USD"
},
"UsageQuantity": {
"Amount": "720042",
"Unit": "N/A"
}
}
},
...
AWS SDK
You can also get the same kind of data programmatically. The easiest way to do it is to use aws sdk. Refer to the documentation of the sdk you want to use. For example information on this functionality for python sdk can be found here.
Specific to invoices, it is unfortunate but still to this day there is no native way to download them other than manually downloading them or being a lucky one to get and have to deal with all of them via email https://aws.amazon.com/premiumsupport/knowledge-center/download-pdf-invoice/
There is https://github.com/iann0036/aws-bill-export (it does not use a native API but instead scrapes the webpage and is setup via lambda and nodejs) and also Puppeteer among other dependencies.
I just finished writing some Python + Selenium that is far more "monstrous" but gets the job done (for today's UI/Jan.2023 at least)...
I thought I'd share both of those since you mentioned them in the OP and no other solutions have come up.
import os
import sys
import time
import argparse
from os.path import expanduser
from datetime import datetime
from dateutil.relativedelta import relativedelta
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.support.ui import WebDriverWait
from selenium.common.exceptions import TimeoutException
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.action_chains import ActionChains
home = expanduser("~")
# Variables grabbed from CLI arguments
parser = argparse.ArgumentParser(
description='AWS Console Login, programming the unprogrammatically-accessible (via CLI/API, using selenium instead).')
parser.add_argument(
'-i', '--interactive',
help="Use False for Headless mode",
default=False,
required=False
)
args = parser.parse_args()
# ChromeDriver options
options = webdriver.ChromeOptions()
if args.interactive == False:
options.add_argument('--headless')
download_directory = "./aws_invoice_downloads"
if not os.path.exists(download_directory):
os.makedirs(download_directory)
else:
download_directory = home + "/Downloads"
options.add_argument("--window-size=1920x1080")
options.add_argument("--remote-debugging-port=9222")
options.add_argument('--no-sandbox')
options.add_argument("--disable-gpu")
options.add_argument('--disable-dev-shm-usage')
options.add_experimental_option("prefs", {
"download.default_directory": download_directory,
"download.prompt_for_download": False
})
# Initiate ChromeDriver
driver = webdriver.Chrome(executable_path='chromedriver', options=options)
# create action chain object
action = ActionChains(driver)
# Set the default selenium timeout
delay = 30 # seconds
# Abort function
def abort_function():
print ("Aborting!")
driver.close()
sys.exit(1)
# Wait for download function
def download_wait(path_to_downloads):
seconds = 0
dl_wait = True
while dl_wait and seconds < 30:
time.sleep(1)
dl_wait = False
for fname in os.listdir(path_to_downloads):
if fname.endswith('.crdownload'):
dl_wait = True
seconds += 1
return seconds
def download_invoices(Id, Network):
print("Switching to the " + Network + "/" + Id + " org account...")
# remove_existing_conflicts(Network)
driver.get("https://signin.aws.amazon.com/switchrole?account=" + Id + "&roleName=YOUR_ROLE_NAME&displayName=" + Network + "%20Org%20Master")
time.sleep(1)
elem = WebDriverWait(driver, delay).until(
EC.presence_of_element_located((By.XPATH, '//*[#type="submit"]'))
)
elem.click()
time.sleep(3)
print("Downloading invoices...")
# Notes
# Can provide YYYY and MM in the URL to get a specific YYYY/MM billing period
# https://us-east-1.console.aws.amazon.com/billing/home?region=us-east-1#/bills?year=2023&month=1
# Get today's YYYY
today = datetime.now()
last_month = today - relativedelta(months=1)
year = last_month.strftime("%Y")
month = last_month.strftime("%m")
driver.get("https://us-east-1.console.aws.amazon.com/billing/home?region=us-east-1#/bills?year=" + year + "&month=" + month)
WebDriverWait(driver, 13).until(
EC.presence_of_element_located((By.XPATH, '//*[#data-testid="main-spinner"]'))
)
time.sleep(2)
elem = WebDriverWait(driver, 13).until(
EC.presence_of_all_elements_located((By.XPATH, '(//*[text()[contains(., " Charges")]])[position() < last() - 1]'))
)
# Count the number of items in the list
elem_count = len(elem)
print("Found " + str(elem_count) + " items in the list...")
# Loop through the list and expand each item
for i in range(1, elem_count + 1):
print("Expanding item " + str(i) + " of " + str(elem_count) + "...")
# (//*[text()[contains(., " Charges")]])[position() < last() - 1][i]
elem = WebDriverWait(driver, 13).until(
EC.presence_of_element_located((By.XPATH, '(//*[text()[contains(., " Charges")]])[position() < last() - 1][' + str(i) + ']'))
)
desired_y = (elem.size['height'] / 2) + elem.location['y']
current_y = (driver.execute_script('return window.innerHeight') / 2) + driver.execute_script('return window.pageYOffset')
scroll_y_by = desired_y - current_y
driver.execute_script("window.scrollBy(0, arguments[0]);", scroll_y_by)
time.sleep(2) # Fixes content shift and ElementClickInterceptedException by waiting, checking the elem, and scrolling again
elem = WebDriverWait(driver, delay).until(
EC.visibility_of_element_located((By.XPATH, '(//*[text()[contains(., " Charges")]])[position() < last() - 1][' + str(i) + ']')))
driver.execute_script("arguments[0].scrollIntoView(true); window.scrollBy(0, -100);", elem)
action.move_to_element(elem).move_by_offset(0,0).click().perform()
# Count the number of invoices with that item
# (//*[text()[contains(., " Charges")]])[position() < last() - 1][2]/following-sibling::div//*[#title="Download Invoice"]
elem = WebDriverWait(driver, 13).until(
EC.presence_of_all_elements_located((By.XPATH, '(//*[text()[contains(., " Charges")]])[position() < last() - 1][' + str(i) + ']/following-sibling::div//*[#title="Download Invoice"]'))
)
# Count the number of items in the list
invoice_count = len(elem)
# Loop through the list and download each invoice
for j in range(1, invoice_count + 1):
print("Downloading invoice " + str(j) + " of " + str(invoice_count) + "...")
# (//*[text()[contains(., " Charges")]])[position() < last() - 1][2]/following-sibling::div//*[#title="Download Invoice"][1]
elem = WebDriverWait(driver, 13).until(
EC.presence_of_element_located((By.XPATH, '((//*[text()[contains(., " Charges")]])[position() < last() - 1][' + str(i) + ']/following-sibling::div//*[#title="Download Invoice"])[' + str(j) + ']'))
)
desired_y = (elem.size['height'] / 2) + elem.location['y']
current_y = (driver.execute_script('return window.innerHeight') / 2) + driver.execute_script('return window.pageYOffset')
scroll_y_by = desired_y - current_y
driver.execute_script("window.scrollBy(0, arguments[0]);", scroll_y_by)
time.sleep(2) # Fixes content shift and ElementClickInterceptedException by waiting, checking the elem, and scrolling again
elem = WebDriverWait(driver, delay).until(
EC.visibility_of_element_located((By.XPATH, '((//*[text()[contains(., " Charges")]])[position() < last() - 1][' + str(i) + ']/following-sibling::div//*[#title="Download Invoice"])[' + str(j) + ']')))
driver.execute_script("arguments[0].scrollIntoView(true); window.scrollBy(0, -100);", elem)
action.move_to_element(elem).move_by_offset(0,0).click().perform()
download_wait(download_directory)
time.sleep(3)
# Find the parent again
elem = WebDriverWait(driver, 13).until(
EC.presence_of_element_located((By.XPATH, '(//*[text()[contains(., " Charges")]])[position() < last() - 1][' + str(i) + ']'))
)
# Collapse the parent
desired_y = (elem.size['height'] / 2) + elem.location['y']
current_y = (driver.execute_script('return window.innerHeight') / 2) + driver.execute_script('return window.pageYOffset')
scroll_y_by = desired_y - current_y
driver.execute_script("window.scrollBy(0, arguments[0]);", scroll_y_by)
time.sleep(2) # Fixes content shift and ElementClickInterceptedException by waiting, checking the elem, and scrolling again
elem = WebDriverWait(driver, delay).until(
EC.visibility_of_element_located((By.XPATH, '(//*[text()[contains(., " Charges")]])[position() < last() - 1][' + str(i) + ']')))
action.move_to_element(elem).move_by_offset(0,0).click().perform()
I created a list of checkboxes with Tkinter but I would like to select all checkboxes with a single checkbox.
Here is part of my code:
root = tk.Tk()
root.title("SOMETHING")
buttons=[]
#If it is checked, then run the file
def callback():
for var, name in buttons:
if var.get():
subprocess.call("python " + "scripts/" + name)
for name in os.listdir("scripts"):
if name.endswith('.py') or name.endswith('.pyc'):
if name not in ("____.py", "_____.pyc"):
var = tk.BooleanVar()
cb = tk.Checkbutton(root, text=name, variable=var)
cb.pack()
buttons.append((var,name))
def select_all():
if var1.get():
for i in cb:
i.select(0,END)
def deselect_all():
if var2.get():
for i in cb:
i.deselect_set(0,END)
var1=tk.BooleanVar()
selectButton = tk.Checkbutton(root, text="Select All", command=select_all, variable=var1)
selectButton.pack()
var2=tk.BooleanVar()
deselectButton = tk.Checkbutton(root, text="None", command=deselect_all, variable=var2)
deselectButton.pack()
submitButton = tk.Button(root, text="Run", command=callback)
submitButton.pack()
root.mainloop()
When I run the file and press "select all", I get this error: 'str' object has no attribute 'select'.
Please help thanks :)
I have reviewed your code and attached a working tkinter code. Will leave you to resolve the issue with using subprocess.
My main comment is that your use of control variable and how to use it to get and set their value was inappropriate. I have remove the unnecessary codes. Also, shown you how to extract information from your list. Hope this helps your tkinter coding journey.
Working code:
import tkinter as tk
import os, subprocess
#If it is checked, then run the file
def callback():
if var.get():
for i in buttons:
cmd = "python3 " + filedir +"/" + i[1] #Note:Runs python3 and not python2
print(cmd)
subprocess.call(cmd)
def select_all(): # Corrected
for item in buttons:
v , n = item
if v.get():
v.set(0)
else:
v.set(1)
root = tk.Tk()
root.title("SOMETHING")
filedir = 'test'
buttons=[]
for name in os.listdir(filedir):
if name.endswith('.py') or name.endswith('.pyc'):
if name not in ("____.py", "_____.pyc"):
var = tk.IntVar()
var.set(0)
cb = tk.Checkbutton(root, text=name, variable=var)
cb.pack()
buttons.append((var,name))
var1=tk.IntVar()
var1.set(0)
selectButton = tk.Checkbutton(root, text="Select All", command=select_all,
variable=var1)
selectButton.pack()
submitButton = tk.Button(root, text="Run", command=callback)
submitButton.pack()
root.mainloop()
I have this script in my base file...
<script src="//tinymce.cachefly.net/4.1/tinymce.min.js"></script>
<script>
tinymce.init({selector:'textarea',
plugins: [
"advlist autolink lists link image charmap print preview hr anchor pagebreak",
"searchreplace wordcount visualblocks visualchars code fullscreen",
"insertdatetime media nonbreaking save table contextmenu directionality",
"emoticons template paste textcolor colorpicker textpattern"
],
});
</script>
In my tinymce text editor I see font of size I guess 10 px but I want to change it to 16 px and also want to change the font family..
Any help ??
To set default font size, just add the line
'content_style': '.mcecontentbody{font-size:13px;}', in TINYMCE_DEFAULT_CONFIG variable of settings.py
TINYMCE_DEFAULT_CONFIG = {
'theme': 'advanced',
'relative_urls': False,
'plugins': 'media,spellchecker',
'content_style': '.mcecontentbody{font-size:13px;}',
'theme_advanced_buttons1': 'bold,italic,underline,bullist,numlist,|,link,unlink,image',
'theme_advanced_resizing': True,
'theme_advanced_path': False,
}
Please see the settings.py config that worked for me:
TINYMCE_DEFAULT_CONFIG = {
'theme': "advanced", # default value
'relative_urls': False, # default value
'plugins': 'table,spellchecker,paste,searchreplace',
'theme_advanced_buttons1': 'bold,italic,underline,bullist,numlist,link,unlink,styleselect,fontselect,fontsizeselect',
'width': '100%',
'height': 300,
'paste_text_sticky': True,
'paste_text_sticky_default': True,
'valid_styles': 'font-weight,font-style,text-decoration',
'fontsize_formats': "8pt 10pt 11pt 12pt 13pt 14pt 16pt 18pt 20pt 24pt 36pt",
'font_formats': "Andale Mono=andale mono,times;" +
"Arial=arial,helvetica,sans-serif;" +
"Arial Black=arial black,avant garde;" +
"Book Antiqua=book antiqua,palatino;" +
"Comic Sans MS=comic sans ms,sans-serif;" +
"Courier New=courier new,courier;" +
"Georgia=georgia,palatino;" +
"Helvetica=helvetica;" +
"Impact=impact,chicago;" +
"Symbol=symbol;" +
"Tahoma=tahoma,arial,helvetica,sans-serif;" +
"Terminal=terminal,monaco;" +
"Times New Roman=times new roman,times;" +
"Trebuchet MS=trebuchet ms,geneva;" +
"Verdana=verdana,geneva;" +
"Webdings=webdings;" +
"Wingdings=wingdings,zapf dingbats",}
TINYMCE_SPELLCHECKER = True
TINYMCE_COMPRESSOR = True
Here is a screenshot:
Take a look at the tinyMCE configuration docs. There are two suitable settings for your needs: font_formats and fontsize_formats and are used like this:
tinymce.init({
fontsize_formats: "8pt 10pt 12pt 14pt 18pt 24pt 36pt",
font_formats: "Arial=arial,helvetica,sans-serif;"
});