From abe6c74933e4987cac7d2e3afb0cfa3b947d1268 Mon Sep 17 00:00:00 2001 From: Gerard Safont Date: Tue, 9 Jun 2026 11:47:09 +0200 Subject: [PATCH 01/11] refactor(image-profile): chage the defoult profile image --- users/static/users/img/Avatar-Exemple.jpeg | Bin 53877 -> 0 bytes users/static/users/img/Avatar-Exemple.webp | Bin 0 -> 6462 bytes 2 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 users/static/users/img/Avatar-Exemple.jpeg create mode 100644 users/static/users/img/Avatar-Exemple.webp diff --git a/users/static/users/img/Avatar-Exemple.jpeg b/users/static/users/img/Avatar-Exemple.jpeg deleted file mode 100644 index e3c6599813aa4ee0b1044bee914f00e7107b153b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 53877 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 diff --git a/users/static/users/img/Avatar-Exemple.webp b/users/static/users/img/Avatar-Exemple.webp new file mode 100644 index 0000000000000000000000000000000000000000..76bbfa3d7487f3259033d050a66c866f37189d2e GIT binary patch literal 6462 zcmYkBWl$X2vV~``!QEYh1eajJ-QC?`aDoIKAV_d`8{FMpf(J`**C4?XoWQ`#IrqML zuWNVr+Fh%@zk64;rh=^OI}!juPexK*S6x6K6#xJb|IN3+e~yH*y8HtY003<04^E+7 zHTiw8ckwsW{#K9S=J$K~#%BWgPVWWt`{NkZ()ab_ZAc=@m_#8uuy;Jd^Yc27sM5m- zbCABvz{BYjDoAsq81W(NQ1Reh?Pfbw@8gt|h=t{92E0Xxcr_tXB356I_mC$@w4Vao zK5#H}+iYvu6x1g=ZqSm2ytqL%jS z9{B|Y)+u!M!d4GX9=#;cbBqi6#T+Cd{7IFBt^j!Vy+@};KMkZHIyA6tCeDl z6H~^}6t+svD=7G!%9H?~T?FJW8z<-0;mRPN4=#KAk%bwPN^(9uvb+3C_uP#PBhH({ zLgrWv+zl$;q^x?^{yux5;$;s!I2DI5U4LEPMv{a!Ju!Q~>zTGmX|3GOw3&3ML;n85 zoiBOz#bcf`&q(&wam!a(`?YJQoP)WAF=PMF_J3kp5T)3VT|ft5DJWukLj^&|F$FST z3AG1EQU!%IdKFj0QE^IoItrU3L?l&E7p|4*N&pDfa*OrQsJ;paSULjhO${GDLetYg z`yPVT&OdH4&oEorEr-Ab*a<-m36o3uH4%oymt(s_(07=OazL=naPWw7E}tvk!pz5J z2f1iN(il>tRxkUdn%0Ikh2>;GFCLOd+rvtZce1Frv+~ocnG&* z@mrYj`JE%oW%{hFqs3eOtJ0<@;rwswX?tpur0c;@$p0p+FwI%plhLaagF>bbq)r%b4 zb>_#!XaoaX9UHfM!~)$5v3`f$rH3Z__nLd8g;QH@7^%Lxf%{9i{eL3r*tm|!A?f3J zbZ>!2JC=6-pm_G1Hov3m@E7ldRmnk-Hlk2}R(#xh{b!*7)RDz#WM1=Z3sDup6 zmn3Xar-bF}g9XOtbY$z2yA|CCUO4EUBcnP{UyhL1Hgvf4NC=LF$4BAB z1jmmMO}!Es(Tq>11iAVK}jMh7Ser;ulAo-lLpodol%tR^6d! zzuU)oj7&R!iwUfvth=Jw0CL2K3{%!1a;gMZHBJy`LjE z9KB;A2fiJZ9mJ94eaYHEI$j9g0cnpdy1`v6gsz}fKU20OUX z$aoWwAI#9i*z=6ey+lsISL`YbNLjM5F*QNkoC?*mlgA~=Q*-#!M=cn<%xv-+L610B z+>(}vP0r-2W>t!Q7v?Ab(ElHNoJ!lyOwFq^(`zA6m)n2!mFGV_t47;ec{$fLHuA#Vm>oiNZ0)?FBXTJWA&oG zI507fzb_)SE)D=Nrw7ObvakYKfuddlnF{p9v^2E1LZ1^cqa0kG12S=XL3T#3-^D5* zw&xsAuXiswuRKpir-6Avr96Ch>$flCuPK*$!9)jUvyjao3pX;uesMFqzOTnx7eQ9A z$U6Yc=(Y4T;a&`mqYe^(2Y6wIrM!$i%zjKb?7xO2Uk5(cKR{q&he5#CS^$y82B7m3!UwVJZ+Q^^6?I^eXUWI4kw2%d`Zv`Wz%wR6Q3O$_hBWGc-@n*&AEW&{M*G~ z;qIrFd*SzvT=@Gv=eOy~oul*?tcR8I^!XxQ(IE~&bfJG}V2Sk7vJlJlW9b_y-Qld? zJ+x_2{+qfG&rYn(tuJR6NMw_P!t8 zJDK88oB{Edl#Wp;0^cs`3@Z_{VxbqTwYCxq_em;DR?y)-#9w*#@XbTLP0!MoHJk3L zk;(=&749PIldxLvUq#x6L6|kxpCeqaCTbxm(^@kMhl@vufd=On9^YFZcWa^rLqGn=w%Rc`r@D02GGbv@x8R0tJ!Vc zEPfhkQ{IIk5*(W))7v$&Osx;c>uVVL+~LL-%I(J~3R%brC&?olRGpyM;R66KFCzt{ zJ|t;YZ@x~>*9fyEv3^g!?mQeSX(;&Ah(C{3;A~g(_+V^NMRq%c$Z=2pW0i$#7MIs@ zyId?kzHg{3v&Cz?tmhcp=K}bd9RS~dacUYE9P}d>K}7err3-CjVn>G&FXTQhqG{5>rYFzvp+4jUzLx9 zzdti{vkP7j)5sPg@RWgSqWqIKy?__X=q;#>oBL|Db9ba*vGdzN zw53FDoG)K?qHFu*o(euZ{+RBmO5oR__}G9*WFQge98}Ft#0rROHr{suNd%qI z+G~9on|{_`-h=88AiI-lJ))bg<5BaWh)j8XH}JmHDgP8S8SKl_iyS@_G}(k36dQ)e zfHN-D<8(^k>H4!FQ9D?*RV&PF^F7o>iWe`ndlklRx><3v&@=d4%oc=wCy zW2kjc{g_d7@TSB4O$`sgicDcU&eX9=b`vf0jdrqL$Kwv9K8tKh`l@|d;M{uW@aK6c z?;Ky%H(X}+9Te7xoLRiMCj0kAu`cgj7=cb_0!awZSeSnl6BK9Be=61$z~3@q6$S2i zpcm6c$KOpjSzbJhjojj(}5bYg}zCo`kCfwi)aP9>%K#p>}?M*M%QZdSz6sUgO2 zxC;vl+?vW7`>Dnq#)!Waw3BZx(vm7B~I@hyLO2H0QCum%Ddqh92eyC6=P_7&4eB}4uWeE;x9yQG-&-tODw&mAc0#JKnC?)J(JFI937i`m_5Eo~hPSNU z;@jcn_bxWB8mDO8C2MJj0NK_)+&ih_pam5n>`T8x-Ldt>MMnThqk#@ib8K zYD@9}FXWcv_0SO=LE`dYhVm1Dsm9{|EBh%qWPF8}g&-t#o{EGosLUSYm*v27tJkEy zrgOa@by&ZEPswW3N4!$;b^d~stt^hn-OK$u0im%tzSJ|1TD|c1LVO@JUf8w=-dKh~ z0l|-6$c&B$9-0k4W^q#Ev1l;!eZ1j-JLOE-SCHHhK}FY9hpwAfX}^E6Wvfg@rBAbi ziw7S!V&rqCY~-;WhZssHBwK|{KYBbR5!&WDm3r5jDzEb!ff-gm#bLF8Q1`{S&i^jd z@zv<2w7o3L7N*~*E`s0wRIiYe8;dCL!8SutXPvN^5rv=lB;xI5$^gw)4uBgb9Mn|| z@L;MK{~oYWS(@F0+Q7X%f&VH#tBFSO=Z6Z7a68YSv{?9uJ|uQ`{tmLnz0D=4Q59wj5Tp|o-3PABfsE~P2=*;wHjY$%%d(- zkr+Aa<0gx&UGuVz;Jei|*t=7Qo9$XYb`&>txNB(&--O5&JK!?67?a~XVf60E2X!-`aVvX@ZHr0 zWGjlqFU$Wb@^vxv{Pk3_x35JYmuO48FM0Nx!l2DGxE0xC&OHUaHZbYb@_NcQVM|%p zL!YZLu_#f(8?pzQUR1ttm*am$B!p8H>o#$sexeJ#o;1CxXg=habPBhD*crQx?4Tt& zyRBY4=ML^SyX?q!BZ_($i2X<-Ph}~t_R-zD_uIr(Z9HgQ#cwI(*wZ~UZ19mAau33g zjEzgwz7|#L&p(6RridSa%BI$(qV4l?>7@?VxBy8Gg_B+~5{oE3TQd z4Rt2i>~UUexDwmybPzH>7QK71V;m>=If-0ibk#nwyAx0Rr4zTgK;v1|ZdYBHnqzly zC!Oow{2oV#{E#449Sp2?_#VYcF5zSJl?3??k=@xq&B0RNsp{t^JIw4Y)oBA*Xfw;Y zUVKNi-3oDjE!(7BeE4l42IjL#owAV>e|X7<=7IwE;>Io@M)Tv457qnW6iIZ^xuf5` zbr#mIC2Bq2VhGHY4^R260-RYUUZ7S_O>T?gh&tq!4V6l$xdA(s*2&>#O(qYFhawx? zv{6ID@u1e_6SiU|L*NlQ_qgsEqM`^^b@}9<^ijvJdgYhuNfG@sj=^Ye{M-jBk{cQ| zVvAZ1aY4h5Y2+Deish!|MYtx~3}SS~ww*+RqSV2X9&8R$ejnc`g!35gkPK+ocGXi$ zP6kquE7HhNiaL>pJxexS$gH~*F5EM%2rNYZLCq04t-JQPz*bF`AAdmX@qriK4LZTl z{uNiGEu9ThusX8Ho`#d3cD^1t zy-+=-Kwa+7fKT=g()46Fs3C|&A2Ua)dK{x5geMHif$r(EgKzsZP5gP{L`-~0qL@?s zdT46omWJ?uw*^NKnB^oj?JZ-3(|SjKKtx|!H2tdm^NW{*;0ppCr2Y9! zezV(U6Jrg-xymW=q6(sB%ZOJJ13EfMCcgp;|HS9{iQhA?BCw3(ha|Xv$xo5!AwoK0 z_a~CHLw;2)YxyZOF#FvkEQ=MkQ7*zDd{$i&UoX^2A@UFtwFG^=*W|}tt{7Cu*r4K^ z^!=LXPzyXd4hK^q!;`R@_QRM&Ivn++A3kiIrtiPX?O8N8vPAGklyUOQNk;96ghtZ? zJ}i@f452#XF^9WYu=&^?^J=50Xi`Op z^ZdGKEXXKw4fhmH&)dvT2R3`O9GAquD8ZBR1^J%n(~ zft1hFkIrwZd>T$V0m{&cz}IP7%0s_XwCL>HC-?hlxxKr9GkhJHU`)M0Eo z^$m&J_XflL>_#8coA+LPfu__!HszC*E3Bdu*X0FoAHl}%q2g<}Aejkpa;Dwv_e846 z-Qk7Brg^vpN#`TsUr}y7gLMrqD3d>S>Z`<>n~QPIW!I{D<L4(2)ITB!=AfttA2}#?Oz&h`ON0&s_1?|u*Atcp;%Fh9yb|i?;|_A z>4%d7^|Pn+t*o{tSdGYL(XQpMYpaQYkoWTBViMM^6tiw91sU&>=4f5--@n(CN2S@C z`xG!0Ng_Y*O(kD~$%|$sBMumfpqLpaDmi)ac5i4i9*np5E(Y$z%{mB7hm_xcp=OsSWlu;Cp=s?>dv zbFocH!;l&uBENkap9aI3BCFATW96S-O#V<(falLN4ihe zkbJ8vE#2QxZ9=ABXwe`UeWseb8dXMhVxpYkBjjw54JB;9Qo-kD%u4RNS?FTyh5}pn zxeS*pza?%*JCqTNl@A(XrD<%xho76@Ub)>JL$h$9=<|6Zu1@$vV_knB^AfSL(btb$ z5FGRspbErqhbT)`?#(U&H>B2L4`D4w$W5ufiS{kQm@O9Emc9Z(LVm@JMU@Y3Q;!2? z?}Yj5HLP7`!P8Bv@}PfHtH$VE#dBKUQ`aEHBxzG=A1RtgQG^16EU0%66w-hfUV!a7 zB}~al5ciady(uC^H}vk2VN3u&y2ER;ZJIH_Brta8?~Xx6UqQC(pZ?Lo764FCU;!Zi zy&(Z`f$#v}U-eIt0ulf1x_>p0C3R%5Ly3X;Qycg2me12PbKXD literal 0 HcmV?d00001 From 986995d9b92289961effc40cd6c1f80b55adb12e Mon Sep 17 00:00:00 2001 From: Gerard Safont Date: Tue, 9 Jun 2026 11:50:19 +0200 Subject: [PATCH 02/11] feat(): create the municipalitites of spain --- data/municipalities_spain.json | 865 ++++++++++++++++++ web_app/management/__init__.py | 0 web_app/management/commands/__init__.py | 0 .../commands/load_municipalities.py | 59 ++ 4 files changed, 924 insertions(+) create mode 100644 data/municipalities_spain.json create mode 100644 web_app/management/__init__.py create mode 100644 web_app/management/commands/__init__.py create mode 100644 web_app/management/commands/load_municipalities.py diff --git a/data/municipalities_spain.json b/data/municipalities_spain.json new file mode 100644 index 0000000..31b1c6e --- /dev/null +++ b/data/municipalities_spain.json @@ -0,0 +1,865 @@ +[ + { + "ine_code": "01", + "name": "Álava", + "municipalities": [ + {"ine_code": "01001", "name": "Alegría-Dulantzi"}, + {"ine_code": "01007", "name": "Gasteiz"}, + {"ine_code": "01059", "name": "Laudio"}, + {"ine_code": "01025", "name": "Amurrio"}, + {"ine_code": "01049", "name": "Llodio"} + ] + }, + { + "ine_code": "02", + "name": "Albacete", + "municipalities": [ + {"ine_code": "02003", "name": "Albacete"}, + {"ine_code": "02024", "name": "Almansa"}, + {"ine_code": "02016", "name": "Hellín"}, + {"ine_code": "02035", "name": "Villarrobledo"}, + {"ine_code": "02029", "name": "La Roda"} + ] + }, + { + "ine_code": "03", + "name": "Alicante", + "municipalities": [ + {"ine_code": "03014", "name": "Alicante"}, + {"ine_code": "03066", "name": "Elche"}, + {"ine_code": "03031", "name": "Benidorm"}, + {"ine_code": "03069", "name": "Elda"}, + {"ine_code": "03088", "name": "Orihuela"}, + {"ine_code": "03119", "name": "Torrevieja"}, + {"ine_code": "03148", "name": "Villajoyosa"} + ] + }, + { + "ine_code": "04", + "name": "Almería", + "municipalities": [ + {"ine_code": "04013", "name": "Almería"}, + {"ine_code": "04034", "name": "El Ejido"}, + {"ine_code": "04065", "name": "Níjar"}, + {"ine_code": "04069", "name": "Roquetas de Mar"} + ] + }, + { + "ine_code": "05", + "name": "Ávila", + "municipalities": [ + {"ine_code": "05019", "name": "Ávila"}, + {"ine_code": "05028", "name": "Arenas de San Pedro"}, + {"ine_code": "05023", "name": "Arévalo"}, + {"ine_code": "05143", "name": "Piedrahíta"} + ] + }, + { + "ine_code": "06", + "name": "Badajoz", + "municipalities": [ + {"ine_code": "06015", "name": "Badajoz"}, + {"ine_code": "06010", "name": "Almendralejo"}, + {"ine_code": "06044", "name": "Don Benito"}, + {"ine_code": "06055", "name": "Mérida"}, + {"ine_code": "06159", "name": "Zafra"} + ] + }, + { + "ine_code": "07", + "name": "Balearic Islands", + "municipalities": [ + {"ine_code": "07022", "name": "Palma"}, + {"ine_code": "07001", "name": "Alcúdia"}, + {"ine_code": "07005", "name": "Calvià"}, + {"ine_code": "07011", "name": "Ibiza"}, + {"ine_code": "07040", "name": "Maó"}, + {"ine_code": "07043", "name": "Manacor"} + ] + }, + { + "ine_code": "08", + "name": "Barcelona", + "municipalities": [ + {"ine_code": "08019", "name": "Barcelona"}, + {"ine_code": "08025", "name": "Badalona"}, + {"ine_code": "08034", "name": "Cornellà de Llobregat"}, + {"ine_code": "08038", "name": "Esplugues de Llobregat"}, + {"ine_code": "08042", "name": "Gavà"}, + {"ine_code": "08045", "name": "Granollers"}, + {"ine_code": "08056", "name": "L'Hospitalet de Llobregat"}, + {"ine_code": "08059", "name": "Igualada"}, + {"ine_code": "08063", "name": "Mataró"}, + {"ine_code": "08069", "name": "Mollet del Vallès"}, + {"ine_code": "08071", "name": "Montcada i Reixac"}, + {"ine_code": "08101", "name": "El Prat de Llobregat"}, + {"ine_code": "08118", "name": "Rubí"}, + {"ine_code": "08120", "name": "Sabadell"}, + {"ine_code": "08128", "name": "Sant Boi de Llobregat"}, + {"ine_code": "08133", "name": "Sant Cugat del Vallès"}, + {"ine_code": "08136", "name": "Sant Feliu de Llobregat"}, + {"ine_code": "08140", "name": "Sant Joan Despí"}, + {"ine_code": "08150", "name": "Sant Vicenç dels Horts"}, + {"ine_code": "08160", "name": "Santa Coloma de Gramenet"}, + {"ine_code": "08184", "name": "Terrassa"}, + {"ine_code": "08200", "name": "Vic"}, + {"ine_code": "08203", "name": "Viladecans"}, + {"ine_code": "08204", "name": "Vilanova i la Geltrú"}, + {"ine_code": "08207", "name": "Cerdanyola del Vallès"} + ] + }, + { + "ine_code": "09", + "name": "Burgos", + "municipalities": [ + {"ine_code": "09059", "name": "Burgos"}, + {"ine_code": "09028", "name": "Aranda de Duero"}, + {"ine_code": "09071", "name": "Briviesca"}, + {"ine_code": "09340", "name": "Villarcayo"} + ] + }, + { + "ine_code": "10", + "name": "Cáceres", + "municipalities": [ + {"ine_code": "10037", "name": "Cáceres"}, + {"ine_code": "10016", "name": "Coria"}, + {"ine_code": "10089", "name": "Plasencia"}, + {"ine_code": "10165", "name": "Trujillo"}, + {"ine_code": "10200", "name": "Navalmoral de la Mata"} + ] + }, + { + "ine_code": "11", + "name": "Cádiz", + "municipalities": [ + {"ine_code": "11012", "name": "Cádiz"}, + {"ine_code": "11001", "name": "Algeciras"}, + {"ine_code": "11004", "name": "Barbate"}, + {"ine_code": "11008", "name": "Chiclana de la Frontera"}, + {"ine_code": "11015", "name": "El Puerto de Santa María"}, + {"ine_code": "11020", "name": "Jerez de la Frontera"}, + {"ine_code": "11022", "name": "La Línea de la Concepción"}, + {"ine_code": "11024", "name": "Rota"}, + {"ine_code": "11031", "name": "San Fernando"}, + {"ine_code": "11033", "name": "Sanlúcar de Barrameda"} + ] + }, + { + "ine_code": "12", + "name": "Castellón", + "municipalities": [ + {"ine_code": "12040", "name": "Castellón de la Plana"}, + {"ine_code": "12001", "name": "Almassora"}, + {"ine_code": "12005", "name": "Benicarló"}, + {"ine_code": "12028", "name": "Borriana"}, + {"ine_code": "12055", "name": "Onda"}, + {"ine_code": "12100", "name": "Vila-real"}, + {"ine_code": "12101", "name": "Vinaròs"} + ] + }, + { + "ine_code": "13", + "name": "Ciudad Real", + "municipalities": [ + {"ine_code": "13034", "name": "Ciudad Real"}, + {"ine_code": "13016", "name": "Alcázar de San Juan"}, + {"ine_code": "13020", "name": "Bolaños de Calatrava"}, + {"ine_code": "13038", "name": "Daimiel"}, + {"ine_code": "13053", "name": "Manzanares"}, + {"ine_code": "13069", "name": "Puertollano"}, + {"ine_code": "13074", "name": "Socuéllamos"}, + {"ine_code": "13079", "name": "Tomelloso"}, + {"ine_code": "13082", "name": "Valdepeñas"} + ] + }, + { + "ine_code": "14", + "name": "Córdoba", + "municipalities": [ + {"ine_code": "14021", "name": "Córdoba"}, + {"ine_code": "14007", "name": "Baena"}, + {"ine_code": "14010", "name": "Cabra"}, + {"ine_code": "14017", "name": "La Carlota"}, + {"ine_code": "14023", "name": "Lucena"}, + {"ine_code": "14024", "name": "Montilla"}, + {"ine_code": "14027", "name": "Montoro"}, + {"ine_code": "14042", "name": "Puente Genil"}, + {"ine_code": "14056", "name": "Priego de Córdoba"} + ] + }, + { + "ine_code": "15", + "name": "A Coruña", + "municipalities": [ + {"ine_code": "15030", "name": "A Coruña"}, + {"ine_code": "15007", "name": "Arteixo"}, + {"ine_code": "15008", "name": "Betanzos"}, + {"ine_code": "15018", "name": "Carballo"}, + {"ine_code": "15019", "name": "Cariño"}, + {"ine_code": "15029", "name": "Culleredo"}, + {"ine_code": "15031", "name": "Ferrol"}, + {"ine_code": "15036", "name": "Narón"}, + {"ine_code": "15038", "name": "Oleiros"}, + {"ine_code": "15044", "name": "Ribeira"}, + {"ine_code": "15049", "name": "Santiago de Compostela"}, + {"ine_code": "15058", "name": "Teo"}, + {"ine_code": "15065", "name": "Oza-Cesuras"} + ] + }, + { + "ine_code": "16", + "name": "Cuenca", + "municipalities": [ + {"ine_code": "16078", "name": "Cuenca"}, + {"ine_code": "16016", "name": "Las Pedroñeras"}, + {"ine_code": "16030", "name": "Mota del Cuervo"}, + {"ine_code": "16054", "name": "San Clemente"}, + {"ine_code": "16085", "name": "Tarancon"} + ] + }, + { + "ine_code": "17", + "name": "Girona", + "municipalities": [ + {"ine_code": "17079", "name": "Girona"}, + {"ine_code": "17001", "name": "Blanes"}, + {"ine_code": "17015", "name": "Banyoles"}, + {"ine_code": "17027", "name": "Figueres"}, + {"ine_code": "17038", "name": "Lloret de Mar"}, + {"ine_code": "17042", "name": "Olot"}, + {"ine_code": "17049", "name": "Palafrugell"}, + {"ine_code": "17053", "name": "Palamós"}, + {"ine_code": "17057", "name": "Puigcerdà"}, + {"ine_code": "17066", "name": "Roses"}, + {"ine_code": "17088", "name": "Salt"}, + {"ine_code": "17134", "name": "Sant Feliu de Guíxols"}, + {"ine_code": "17200", "name": "Santa Coloma de Farners"} + ] + }, + { + "ine_code": "18", + "name": "Granada", + "municipalities": [ + {"ine_code": "18087", "name": "Granada"}, + {"ine_code": "18006", "name": "Albolote"}, + {"ine_code": "18015", "name": "Almuñécar"}, + {"ine_code": "18020", "name": "Armilla"}, + {"ine_code": "18029", "name": "Baza"}, + {"ine_code": "18033", "name": "La Zubia"}, + {"ine_code": "18076", "name": "Guadix"}, + {"ine_code": "18091", "name": "Huescar"}, + {"ine_code": "18126", "name": "Loja"}, + {"ine_code": "18144", "name": "Maracena"}, + {"ine_code": "18152", "name": "Motril"}, + {"ine_code": "18199", "name": "Santa Fe"}, + {"ine_code": "18224", "name": "Ogíjares"} + ] + }, + { + "ine_code": "19", + "name": "Guadalajara", + "municipalities": [ + {"ine_code": "19130", "name": "Guadalajara"}, + {"ine_code": "19003", "name": "Azuqueca de Henares"}, + {"ine_code": "19024", "name": "Cabanillas del Campo"}, + {"ine_code": "19251", "name": "El Casar"}, + {"ine_code": "19229", "name": "Sigüenza"}, + {"ine_code": "19045", "name": "Marchamalo"}, + {"ine_code": "19088", "name": "Torrejón del Rey"} + ] + }, + { + "ine_code": "20", + "name": "Gipuzkoa", + "municipalities": [ + {"ine_code": "20069", "name": "Donostia / San Sebastián"}, + {"ine_code": "20017", "name": "Andoain"}, + {"ine_code": "20024", "name": "Arrasate / Mondragón"}, + {"ine_code": "20036", "name": "Eibar"}, + {"ine_code": "20054", "name": "Errenteria"}, + {"ine_code": "20065", "name": "Hondarribia"}, + {"ine_code": "20067", "name": "Irun"}, + {"ine_code": "20071", "name": "Lasarte-Oria"}, + {"ine_code": "20076", "name": "Oñati"}, + {"ine_code": "20063", "name": "Pasaia"}, + {"ine_code": "20078", "name": "Zarautz"} + ] + }, + { + "ine_code": "21", + "name": "Huelva", + "municipalities": [ + {"ine_code": "21041", "name": "Huelva"}, + {"ine_code": "21002", "name": "Aljaraque"}, + {"ine_code": "21005", "name": "Almonte"}, + {"ine_code": "21026", "name": "Bollullos Par del Condado"}, + {"ine_code": "21032", "name": "Cartaya"}, + {"ine_code": "21043", "name": "Isla Cristina"}, + {"ine_code": "21055", "name": "Lepe"}, + {"ine_code": "21056", "name": "Moguer"}, + {"ine_code": "21071", "name": "Punta Umbría"}, + {"ine_code": "21077", "name": "San Juan del Puerto"} + ] + }, + { + "ine_code": "22", + "name": "Huesca", + "municipalities": [ + {"ine_code": "22125", "name": "Huesca"}, + {"ine_code": "22014", "name": "Barbastro"}, + {"ine_code": "22017", "name": "Binefar"}, + {"ine_code": "22053", "name": "Fraga"}, + {"ine_code": "22097", "name": "Jaca"}, + {"ine_code": "22205", "name": "Monzón"}, + {"ine_code": "22225", "name": "Sabiñánigo"} + ] + }, + { + "ine_code": "23", + "name": "Jaén", + "municipalities": [ + {"ine_code": "23050", "name": "Jaén"}, + {"ine_code": "23006", "name": "Alcalá la Real"}, + {"ine_code": "23008", "name": "Andújar"}, + {"ine_code": "23009", "name": "Arjona"}, + {"ine_code": "23016", "name": "Baeza"}, + {"ine_code": "23019", "name": "Bailén"}, + {"ine_code": "23030", "name": "La Carolina"}, + {"ine_code": "23039", "name": "Linares"}, + {"ine_code": "23044", "name": "Martos"}, + {"ine_code": "23047", "name": "Mengíbar"}, + {"ine_code": "23056", "name": "Torredonjimeno"}, + {"ine_code": "23060", "name": "Úbeda"}, + {"ine_code": "23069", "name": "Villacarrillo"} + ] + }, + { + "ine_code": "24", + "name": "León", + "municipalities": [ + {"ine_code": "24089", "name": "León"}, + {"ine_code": "24009", "name": "Astorga"}, + {"ine_code": "24020", "name": "Bembibre"}, + {"ine_code": "24023", "name": "La Bañeza"}, + {"ine_code": "24035", "name": "Cistierna"}, + {"ine_code": "24044", "name": "Ponferrada"}, + {"ine_code": "24108", "name": "San Andrés del Rabanedo"}, + {"ine_code": "24154", "name": "Villaquilambre"}, + {"ine_code": "24151", "name": "Valencia de Don Juan"} + ] + }, + { + "ine_code": "25", + "name": "Lleida", + "municipalities": [ + {"ine_code": "25120", "name": "Lleida"}, + {"ine_code": "25007", "name": "Alcarràs"}, + {"ine_code": "25010", "name": "Alfarràs"}, + {"ine_code": "25025", "name": "Balaguer"}, + {"ine_code": "25038", "name": "Cervera"}, + {"ine_code": "25045", "name": "Les Borges Blanques"}, + {"ine_code": "25073", "name": "Mollerussa"}, + {"ine_code": "25084", "name": "La Seu d'Urgell"}, + {"ine_code": "25117", "name": "Solsona"}, + {"ine_code": "25134", "name": "Tàrrega"}, + {"ine_code": "25209", "name": "Vielha e Mijaran"} + ] + }, + { + "ine_code": "26", + "name": "La Rioja", + "municipalities": [ + {"ine_code": "26089", "name": "Logroño"}, + {"ine_code": "26007", "name": "Alfaro"}, + {"ine_code": "26012", "name": "Arnedo"}, + {"ine_code": "26021", "name": "Calahorra"}, + {"ine_code": "26056", "name": "Haro"}, + {"ine_code": "26059", "name": "Lardero"}, + {"ine_code": "26063", "name": "Nájera"}, + {"ine_code": "26079", "name": "Santo Domingo de la Calzada"} + ] + }, + { + "ine_code": "27", + "name": "Lugo", + "municipalities": [ + {"ine_code": "27028", "name": "Lugo"}, + {"ine_code": "27009", "name": "Burela"}, + {"ine_code": "27010", "name": "Castro de Rei"}, + {"ine_code": "27015", "name": "Cervo"}, + {"ine_code": "27018", "name": "Chantada"}, + {"ine_code": "27020", "name": "A Fonsagrada"}, + {"ine_code": "27023", "name": "Guitiriz"}, + {"ine_code": "27027", "name": "Monforte de Lemos"}, + {"ine_code": "27029", "name": "Palas de Rei"}, + {"ine_code": "27035", "name": "Ribadeo"}, + {"ine_code": "27038", "name": "Sarria"}, + {"ine_code": "27040", "name": "Vilalba"}, + {"ine_code": "27041", "name": "Viveiro"} + ] + }, + { + "ine_code": "28", + "name": "Madrid", + "municipalities": [ + {"ine_code": "28079", "name": "Madrid"}, + {"ine_code": "28002", "name": "Alcalá de Henares"}, + {"ine_code": "28005", "name": "Alcobendas"}, + {"ine_code": "28006", "name": "Alcorcón"}, + {"ine_code": "28007", "name": "Algete"}, + {"ine_code": "28013", "name": "Aranjuez"}, + {"ine_code": "28014", "name": "Arganda del Rey"}, + {"ine_code": "28016", "name": "Boadilla del Monte"}, + {"ine_code": "28021", "name": "Collado Villalba"}, + {"ine_code": "28023", "name": "Coslada"}, + {"ine_code": "28025", "name": "El Escorial"}, + {"ine_code": "28026", "name": "Fuenlabrada"}, + {"ine_code": "28029", "name": "Getafe"}, + {"ine_code": "28030", "name": "Guadarrama"}, + {"ine_code": "28031", "name": "Humanes de Madrid"}, + {"ine_code": "28034", "name": "Las Rozas de Madrid"}, + {"ine_code": "28035", "name": "Leganés"}, + {"ine_code": "28040", "name": "Móstoles"}, + {"ine_code": "28046", "name": "Parla"}, + {"ine_code": "28047", "name": "Pinto"}, + {"ine_code": "28049", "name": "Pozuelo de Alarcón"}, + {"ine_code": "28052", "name": "Rivas-Vaciamadrid"}, + {"ine_code": "28053", "name": "San Fernando de Henares"}, + {"ine_code": "28056", "name": "San Sebastián de los Reyes"}, + {"ine_code": "28065", "name": "Torrejón de Ardoz"}, + {"ine_code": "28067", "name": "Tres Cantos"}, + {"ine_code": "28070", "name": "Valdemoro"}, + {"ine_code": "28078", "name": "Majadahonda"} + ] + }, + { + "ine_code": "29", + "name": "Málaga", + "municipalities": [ + {"ine_code": "29067", "name": "Málaga"}, + {"ine_code": "29003", "name": "Alhaurín de la Torre"}, + {"ine_code": "29004", "name": "Alhaurín el Grande"}, + {"ine_code": "29013", "name": "Antequera"}, + {"ine_code": "29018", "name": "Benalmádena"}, + {"ine_code": "29020", "name": "Cártama"}, + {"ine_code": "29024", "name": "Coín"}, + {"ine_code": "29028", "name": "Estepona"}, + {"ine_code": "29030", "name": "Fuengirola"}, + {"ine_code": "29033", "name": "Marbella"}, + {"ine_code": "29041", "name": "Mijas"}, + {"ine_code": "29051", "name": "Rincón de la Victoria"}, + {"ine_code": "29055", "name": "Ronda"}, + {"ine_code": "29059", "name": "Torremolinos"}, + {"ine_code": "29062", "name": "Vélez-Málaga"} + ] + }, + { + "ine_code": "30", + "name": "Murcia", + "municipalities": [ + {"ine_code": "30030", "name": "Murcia"}, + {"ine_code": "30012", "name": "Alcantarilla"}, + {"ine_code": "30014", "name": "Alhama de Murcia"}, + {"ine_code": "30019", "name": "Cieza"}, + {"ine_code": "30022", "name": "Jumilla"}, + {"ine_code": "30024", "name": "Lorca"}, + {"ine_code": "30027", "name": "Molina de Segura"}, + {"ine_code": "30028", "name": "San Javier"}, + {"ine_code": "30033", "name": "Torre-Pacheco"}, + {"ine_code": "30035", "name": "Totana"}, + {"ine_code": "30041", "name": "Yecla"}, + {"ine_code": "30042", "name": "Las Torres de Cotillas"}, + {"ine_code": "30044", "name": "San Pedro del Pinatar"} + ] + }, + { + "ine_code": "31", + "name": "Navarre", + "municipalities": [ + {"ine_code": "31201", "name": "Pamplona"}, + {"ine_code": "31041", "name": "Barañáin"}, + {"ine_code": "31047", "name": "Burlada"}, + {"ine_code": "31061", "name": "Egüés"}, + {"ine_code": "31083", "name": "Estella-Lizarra"}, + {"ine_code": "31154", "name": "Tafalla"}, + {"ine_code": "31159", "name": "Tudela"}, + {"ine_code": "31170", "name": "Villava"}, + {"ine_code": "31251", "name": "Zizur Mayor"} + ] + }, + { + "ine_code": "32", + "name": "Ourense", + "municipalities": [ + {"ine_code": "32054", "name": "Ourense"}, + {"ine_code": "32001", "name": "Allariz"}, + {"ine_code": "32008", "name": "Barbadás"}, + {"ine_code": "32009", "name": "O Barco de Valdeorras"}, + {"ine_code": "32013", "name": "O Carballiño"}, + {"ine_code": "32018", "name": "Celanova"}, + {"ine_code": "32028", "name": "Xinzo de Limia"}, + {"ine_code": "32044", "name": "Nogueira de Ramuín"}, + {"ine_code": "32046", "name": "O Pereiro de Aguiar"}, + {"ine_code": "32050", "name": "A Pobra de Trives"}, + {"ine_code": "32055", "name": "Ribadavia"}, + {"ine_code": "32059", "name": "Rubiá"}, + {"ine_code": "32067", "name": "Verín"}, + {"ine_code": "32068", "name": "Viana do Bolo"}, + {"ine_code": "32072", "name": "Xinzo de Limia"} + ] + }, + { + "ine_code": "33", + "name": "Asturias", + "municipalities": [ + {"ine_code": "33044", "name": "Oviedo"}, + {"ine_code": "33005", "name": "Avilés"}, + {"ine_code": "33010", "name": "Cangas del Narcea"}, + {"ine_code": "33012", "name": "Carreño"}, + {"ine_code": "33014", "name": "Castrillón"}, + {"ine_code": "33016", "name": "Corvera de Asturias"}, + {"ine_code": "33019", "name": "Gijón"}, + {"ine_code": "33022", "name": "Grado"}, + {"ine_code": "33024", "name": "Langreo"}, + {"ine_code": "33026", "name": "Laviana"}, + {"ine_code": "33028", "name": "Llanera"}, + {"ine_code": "33032", "name": "Mieres"}, + {"ine_code": "33034", "name": "Navia"}, + {"ine_code": "33037", "name": "Pravia"}, + {"ine_code": "33041", "name": "Ribadesella"}, + {"ine_code": "33046", "name": "San Martín del Rey Aurelio"}, + {"ine_code": "33052", "name": "Siero"}, + {"ine_code": "33059", "name": "Tineo"}, + {"ine_code": "33061", "name": "Valdés"}, + {"ine_code": "33065", "name": "Villaviciosa"} + ] + }, + { + "ine_code": "34", + "name": "Palencia", + "municipalities": [ + {"ine_code": "34120", "name": "Palencia"}, + {"ine_code": "34004", "name": "Ampudia"}, + {"ine_code": "34019", "name": "Carrión de los Condes"}, + {"ine_code": "34023", "name": "Cervera de Pisuerga"}, + {"ine_code": "34027", "name": "Guardo"}, + {"ine_code": "34046", "name": "Venta de Baños"}, + {"ine_code": "34048", "name": "Villamuriel de Cerrato"} + ] + }, + { + "ine_code": "35", + "name": "Las Palmas", + "municipalities": [ + {"ine_code": "35016", "name": "Las Palmas de Gran Canaria"}, + {"ine_code": "35001", "name": "Arucas"}, + {"ine_code": "35004", "name": "Arrecife"}, + {"ine_code": "35006", "name": "Ingenio"}, + {"ine_code": "35011", "name": "San Bartolomé de Tirajana"}, + {"ine_code": "35013", "name": "Santa Lucía de Tirajana"}, + {"ine_code": "35017", "name": "Telde"}, + {"ine_code": "35020", "name": "Yaiza"}, + {"ine_code": "35021", "name": "Agüimes"}, + {"ine_code": "35022", "name": "Teguise"}, + {"ine_code": "35023", "name": "Puerto del Rosario"} + ] + }, + { + "ine_code": "36", + "name": "Pontevedra", + "municipalities": [ + {"ine_code": "36057", "name": "Pontevedra"}, + {"ine_code": "36002", "name": "Baiona"}, + {"ine_code": "36007", "name": "Cambados"}, + {"ine_code": "36010", "name": "Cangas"}, + {"ine_code": "36017", "name": "A Estrada"}, + {"ine_code": "36021", "name": "O Grove"}, + {"ine_code": "36022", "name": "A Guarda"}, + {"ine_code": "36024", "name": "Lalín"}, + {"ine_code": "36026", "name": "Marín"}, + {"ine_code": "36029", "name": "Moaña"}, + {"ine_code": "36033", "name": "Mos"}, + {"ine_code": "36035", "name": "Nigrán"}, + {"ine_code": "36038", "name": "Poio"}, + {"ine_code": "36040", "name": "Ponteareas"}, + {"ine_code": "36042", "name": "O Porriño"}, + {"ine_code": "36044", "name": "Redondela"}, + {"ine_code": "36050", "name": "Sanxenxo"}, + {"ine_code": "36054", "name": "Tui"}, + {"ine_code": "36056", "name": "Vigo"}, + {"ine_code": "36058", "name": "Vilaboa"}, + {"ine_code": "36060", "name": "Vilagarcía de Arousa"}, + {"ine_code": "36061", "name": "Vilanova de Arousa"} + ] + }, + { + "ine_code": "37", + "name": "Salamanca", + "municipalities": [ + {"ine_code": "37274", "name": "Salamanca"}, + {"ine_code": "37007", "name": "Alba de Tormes"}, + {"ine_code": "37022", "name": "Béjar"}, + {"ine_code": "37039", "name": "Carbajosa de la Sagrada"}, + {"ine_code": "37069", "name": "Ciudad Rodrigo"}, + {"ine_code": "37160", "name": "Peñaranda de Bracamonte"}, + {"ine_code": "37233", "name": "Santa Marta de Tormes"} + ] + }, + { + "ine_code": "38", + "name": "Santa Cruz de Tenerife", + "municipalities": [ + {"ine_code": "38038", "name": "Santa Cruz de Tenerife"}, + {"ine_code": "38001", "name": "Adeje"}, + {"ine_code": "38004", "name": "Arona"}, + {"ine_code": "38006", "name": "Candelaria"}, + {"ine_code": "38012", "name": "Granadilla de Abona"}, + {"ine_code": "38017", "name": "Icod de los Vinos"}, + {"ine_code": "38018", "name": "San Cristóbal de La Laguna"}, + {"ine_code": "38020", "name": "La Orotava"}, + {"ine_code": "38023", "name": "Puerto de la Cruz"}, + {"ine_code": "38027", "name": "Santa Cruz de la Palma"}, + {"ine_code": "38030", "name": "Tacoronte"} + ] + }, + { + "ine_code": "39", + "name": "Cantabria", + "municipalities": [ + {"ine_code": "39075", "name": "Santander"}, + {"ine_code": "39008", "name": "El Astillero"}, + {"ine_code": "39012", "name": "Cabezón de la Sal"}, + {"ine_code": "39016", "name": "Camargo"}, + {"ine_code": "39020", "name": "Castro Urdiales"}, + {"ine_code": "39022", "name": "Comillas"}, + {"ine_code": "39032", "name": "Laredo"}, + {"ine_code": "39044", "name": "Noja"}, + {"ine_code": "39049", "name": "Piélagos"}, + {"ine_code": "39056", "name": "Reinosa"}, + {"ine_code": "39070", "name": "Santa Cruz de Bezana"}, + {"ine_code": "39072", "name": "Santillana del Mar"}, + {"ine_code": "39076", "name": "Santoña"}, + {"ine_code": "39083", "name": "Torrelavega"} + ] + }, + { + "ine_code": "40", + "name": "Segovia", + "municipalities": [ + {"ine_code": "40194", "name": "Segovia"}, + {"ine_code": "40002", "name": "Ayllón"}, + {"ine_code": "40014", "name": "Carbonero el Mayor"}, + {"ine_code": "40018", "name": "Cuéllar"}, + {"ine_code": "40032", "name": "El Espinar"}, + {"ine_code": "40038", "name": "La Lastrilla"}, + {"ine_code": "40086", "name": "Real Sitio de San Ildefonso"} + ] + }, + { + "ine_code": "41", + "name": "Seville", + "municipalities": [ + {"ine_code": "41091", "name": "Seville"}, + {"ine_code": "41002", "name": "Alcalá de Guadaíra"}, + {"ine_code": "41006", "name": "Camas"}, + {"ine_code": "41010", "name": "Dos Hermanas"}, + {"ine_code": "41011", "name": "Écija"}, + {"ine_code": "41019", "name": "Lebrija"}, + {"ine_code": "41021", "name": "Mairena del Aljarafe"}, + {"ine_code": "41022", "name": "Mairena del Alcor"}, + {"ine_code": "41051", "name": "La Rinconada"}, + {"ine_code": "41037", "name": "Utrera"}, + {"ine_code": "41035", "name": "Tomares"} + ] + }, + { + "ine_code": "42", + "name": "Soria", + "municipalities": [ + {"ine_code": "42173", "name": "Soria"}, + {"ine_code": "42006", "name": "Almazán"}, + {"ine_code": "42020", "name": "El Burgo de Osma"}, + {"ine_code": "42071", "name": "Golmayo"}, + {"ine_code": "42080", "name": "Medinaceli"} + ] + }, + { + "ine_code": "43", + "name": "Tarragona", + "municipalities": [ + {"ine_code": "43148", "name": "Tarragona"}, + {"ine_code": "43006", "name": "L'Ampolla"}, + {"ine_code": "43007", "name": "Amposta"}, + {"ine_code": "43017", "name": "Calafell"}, + {"ine_code": "43019", "name": "Cambrils"}, + {"ine_code": "43023", "name": "Cunit"}, + {"ine_code": "43040", "name": "El Vendrell"}, + {"ine_code": "43057", "name": "Reus"}, + {"ine_code": "43062", "name": "Salou"}, + {"ine_code": "43063", "name": "Sant Carles de la Ràpita"}, + {"ine_code": "43070", "name": "Torredembarra"}, + {"ine_code": "43071", "name": "Valls"}, + {"ine_code": "43073", "name": "Vila-seca"} + ] + }, + { + "ine_code": "44", + "name": "Teruel", + "municipalities": [ + {"ine_code": "44216", "name": "Teruel"}, + {"ine_code": "44006", "name": "Albarracín"}, + {"ine_code": "44017", "name": "Alcorisa"}, + {"ine_code": "44054", "name": "Calamocha"}, + {"ine_code": "44060", "name": "Cella"} + ] + }, + { + "ine_code": "45", + "name": "Toledo", + "municipalities": [ + {"ine_code": "45168", "name": "Toledo"}, + {"ine_code": "45008", "name": "Consuegra"}, + {"ine_code": "45018", "name": "Illescas"}, + {"ine_code": "45021", "name": "Madridejos"}, + {"ine_code": "45031", "name": "Mora"}, + {"ine_code": "45042", "name": "Ocaña"}, + {"ine_code": "45063", "name": "Quintanar de la Orden"}, + {"ine_code": "45082", "name": "Seseña"}, + {"ine_code": "45086", "name": "Talavera de la Reina"}, + {"ine_code": "45092", "name": "Torrijos"}, + {"ine_code": "45104", "name": "Villacañas"}, + {"ine_code": "45122", "name": "Yuncos"} + ] + }, + { + "ine_code": "46", + "name": "Valencia", + "municipalities": [ + {"ine_code": "46250", "name": "Valencia"}, + {"ine_code": "46001", "name": "Alaquàs"}, + {"ine_code": "46002", "name": "Alboraia"}, + {"ine_code": "46006", "name": "Alfafar"}, + {"ine_code": "46011", "name": "Algemesí"}, + {"ine_code": "46014", "name": "Almàssera"}, + {"ine_code": "46027", "name": "Benaguasil"}, + {"ine_code": "46034", "name": "Benifaió"}, + {"ine_code": "46050", "name": "Burjassot"}, + {"ine_code": "46054", "name": "Carcaixent"}, + {"ine_code": "46056", "name": "Carlet"}, + {"ine_code": "46066", "name": "Catarroja"}, + {"ine_code": "46077", "name": "Cullera"}, + {"ine_code": "46080", "name": "L'Eliana"}, + {"ine_code": "46094", "name": "Gandia"}, + {"ine_code": "46100", "name": "Godella"}, + {"ine_code": "46111", "name": "Llíria"}, + {"ine_code": "46119", "name": "Manises"}, + {"ine_code": "46122", "name": "Massalavés"}, + {"ine_code": "46125", "name": "Massanassa"}, + {"ine_code": "46129", "name": "Mislata"}, + {"ine_code": "46139", "name": "Oliva"}, + {"ine_code": "46142", "name": "Ontinyent"}, + {"ine_code": "46144", "name": "Paiporta"}, + {"ine_code": "46147", "name": "Paterna"}, + {"ine_code": "46150", "name": "Picanya"}, + {"ine_code": "46151", "name": "Picassent"}, + {"ine_code": "46155", "name": "La Pobla de Vallbona"}, + {"ine_code": "46164", "name": "Quart de Poblet"}, + {"ine_code": "46168", "name": "Rafelbunyol"}, + {"ine_code": "46172", "name": "Riba-roja de Túria"}, + {"ine_code": "46178", "name": "Sagunt"}, + {"ine_code": "46182", "name": "Sedaví"}, + {"ine_code": "46194", "name": "Sueca"}, + {"ine_code": "46196", "name": "Tavernes Blanques"}, + {"ine_code": "46197", "name": "Tavernes de la Valldigna"}, + {"ine_code": "46203", "name": "Torrent"}, + {"ine_code": "46209", "name": "Valencia"}, + {"ine_code": "46220", "name": "Xàtiva"}, + {"ine_code": "46223", "name": "Xirivella"} + ] + }, + { + "ine_code": "47", + "name": "Valladolid", + "municipalities": [ + {"ine_code": "47186", "name": "Valladolid"}, + {"ine_code": "47003", "name": "Arroyo de la Encomienda"}, + {"ine_code": "47017", "name": "La Cistérniga"}, + {"ine_code": "47023", "name": "Laguna de Duero"}, + {"ine_code": "47028", "name": "Medina del Campo"}, + {"ine_code": "47044", "name": "Peñafiel"}, + {"ine_code": "47060", "name": "Simancas"}, + {"ine_code": "47061", "name": "Tordesillas"}, + {"ine_code": "47075", "name": "Tudela de Duero"} + ] + }, + { + "ine_code": "48", + "name": "Biscay", + "municipalities": [ + {"ine_code": "48020", "name": "Bilbao"}, + {"ine_code": "48001", "name": "Abadiño"}, + {"ine_code": "48002", "name": "Amorebieta-Etxano"}, + {"ine_code": "48004", "name": "Arrigorriaga"}, + {"ine_code": "48005", "name": "Barakaldo"}, + {"ine_code": "48006", "name": "Basauri"}, + {"ine_code": "48010", "name": "Durango"}, + {"ine_code": "48011", "name": "Erandio"}, + {"ine_code": "48013", "name": "Galdakao"}, + {"ine_code": "48014", "name": "Gernika-Lumo"}, + {"ine_code": "48015", "name": "Getxo"}, + {"ine_code": "48018", "name": "Leioa"}, + {"ine_code": "48021", "name": "Muskiz"}, + {"ine_code": "48023", "name": "Portugalete"}, + {"ine_code": "48024", "name": "Santurtzi"}, + {"ine_code": "48025", "name": "Sestao"}, + {"ine_code": "48026", "name": "Sopela"}, + {"ine_code": "48027", "name": "Trapagaran"}, + {"ine_code": "48028", "name": "Zornotza"} + ] + }, + { + "ine_code": "49", + "name": "Zamora", + "municipalities": [ + {"ine_code": "49175", "name": "Zamora"}, + {"ine_code": "49005", "name": "Benavente"}, + {"ine_code": "49009", "name": "Fuentesaúco"}, + {"ine_code": "49012", "name": "Puebla de Sanabria"}, + {"ine_code": "49014", "name": "Toro"} + ] + }, + { + "ine_code": "50", + "name": "Zaragoza", + "municipalities": [ + {"ine_code": "50297", "name": "Zaragoza"}, + {"ine_code": "50001", "name": "Alagón"}, + {"ine_code": "50006", "name": "Calatayud"}, + {"ine_code": "50007", "name": "Caspe"}, + {"ine_code": "50009", "name": "Ejea de los Caballeros"}, + {"ine_code": "50010", "name": "Épila"}, + {"ine_code": "50012", "name": "Jaulín"}, + {"ine_code": "50013", "name": "La Almunia de Doña Godina"}, + {"ine_code": "50014", "name": "La Muela"}, + {"ine_code": "50020", "name": "Puebla de Alfindén"}, + {"ine_code": "50024", "name": "Tarazona"}, + {"ine_code": "50026", "name": "Utebo"}, + {"ine_code": "50028", "name": "Zuera"} + ] + }, + { + "ine_code": "51", + "name": "Ceuta", + "municipalities": [ + {"ine_code": "51001", "name": "Ceuta"} + ] + }, + { + "ine_code": "52", + "name": "Melilla", + "municipalities": [ + {"ine_code": "52001", "name": "Melilla"} + ] + } +] diff --git a/web_app/management/__init__.py b/web_app/management/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/web_app/management/commands/__init__.py b/web_app/management/commands/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/web_app/management/commands/load_municipalities.py b/web_app/management/commands/load_municipalities.py new file mode 100644 index 0000000..eefe039 --- /dev/null +++ b/web_app/management/commands/load_municipalities.py @@ -0,0 +1,59 @@ +import json +from pathlib import Path + +from django.core.management.base import BaseCommand + +from web_app.models import Province, Municipality + + +class Command(BaseCommand): + help = "Loads provinces and municipalities from a JSON file into the database." + + def add_arguments(self, parser): + parser.add_argument( + '--source', + type=str, + default=None, + help='Path to the JSON data file. Defaults to data/municipalities_spain.json', + ) + + def handle(self, *args, **options): + source = options.get('source') + if not source: + source = Path(__file__).resolve().parents[3] / 'data' / 'municipalities_spain.json' + + source_path = Path(source) + + if not source_path.exists(): + self.stderr.write(self.style.ERROR(f"File not found: {source_path}")) + return + + with open(source_path, encoding='utf-8') as f: + data = json.load(f) + + provinces_created = 0 + municipalities_created = 0 + + for province_data in data: + province, created = Province.objects.update_or_create( + ine_code=province_data['ine_code'], + defaults={'name': province_data['name']}, + ) + if created: + provinces_created += 1 + self.stdout.write(f" Created province: {province.name}") + + for municipality_data in province_data.get('municipalities', []): + municipality, created = Municipality.objects.update_or_create( + ine_code=municipality_data['ine_code'], + defaults={ + 'name': municipality_data['name'], + 'province': province, + }, + ) + if created: + municipalities_created += 1 + + self.stdout.write(self.style.SUCCESS( + f"Done. {provinces_created} provinces and {municipalities_created} municipalities loaded." + )) From aff5f25f2c8d89b08a9fef8fac29e305fad212e1 Mon Sep 17 00:00:00 2001 From: Gerard Safont Date: Tue, 9 Jun 2026 11:54:08 +0200 Subject: [PATCH 03/11] feat(db-forms):add new information of user --- web_app/admin.py | 15 +++- web_app/forms.py | 78 ++++++++++++++++++- ...nce_remove_customuser_location_and_more.py | 64 +++++++++++++++ web_app/models.py | 58 +++++++++++++- web_app/urls.py | 2 + web_app/views.py | 17 +++- 6 files changed, 224 insertions(+), 10 deletions(-) create mode 100644 web_app/migrations/0004_municipality_province_remove_customuser_location_and_more.py diff --git a/web_app/admin.py b/web_app/admin.py index 9203990..4d6c700 100644 --- a/web_app/admin.py +++ b/web_app/admin.py @@ -1,6 +1,6 @@ from django.contrib import admin from django.utils.safestring import mark_safe -from .models import SyncLog, API, Contingut, Movie, Series, Director, Genre, AgeRating, CustomUser, UserProfile, Valoracio, Preferits, RoleAuditLog +from .models import SyncLog, API, Contingut, Movie, Series, Director, Genre, AgeRating, CustomUser, UserProfile, Valoracio, Preferits, RoleAuditLog, Province, Municipality @admin.register(API) @@ -136,4 +136,15 @@ class RoleAuditLogAdmin(admin.ModelAdmin): list_display = ('timestamp', 'admin', 'affected_user', 'old_role', 'new_role') list_filter = ('timestamp', 'old_role', 'new_role') search_fields = ('admin__username', 'affected_user__username') - readonly_fields = ('admin', 'affected_user', 'old_role', 'new_role', 'timestamp') \ No newline at end of file + readonly_fields = ('admin', 'affected_user', 'old_role', 'new_role', 'timestamp') + +@admin.register(Province) +class ProvinceAdmin(admin.ModelAdmin): + list_display = ('ine_code', 'name') + search_fields = ('name',) + +@admin.register(Municipality) +class MunicipalityAdmin(admin.ModelAdmin): + list_display = ('ine_code', 'name', 'province') + list_filter = ('province',) + search_fields = ('name',) \ No newline at end of file diff --git a/web_app/forms.py b/web_app/forms.py index 5a49b38..a2f4bbb 100644 --- a/web_app/forms.py +++ b/web_app/forms.py @@ -4,7 +4,7 @@ from django.core.exceptions import ValidationError from django.core.validators import MaxLengthValidator -from .models import CustomUser, API, UserType +from .models import CustomUser, API, UserType, Gender, AgeRange, Province, Municipality class CustomUserCreationForm(forms.ModelForm): """ @@ -45,9 +45,29 @@ class CustomUserCreationForm(forms.ModelForm): label=_("Select your platforms") ) + gender = forms.ChoiceField( + choices=Gender.choices, + label=_("Gender"), + required=True, + ) + + age_range = forms.ChoiceField( + choices=AgeRange.choices, + label=_("Age Range"), + required=True, + ) + + province = forms.ModelChoiceField( + queryset=Province.objects.all(), + label=_("Province"), + required=True, + ) + + class Meta: model = CustomUser - fields = ('username', 'email', 'password', 'password2', 'platforms') + fields = ('username', 'email', 'password', 'password2', 'platforms', + 'gender', 'age_range', 'province', 'municipality') def clean_password2(self): """ @@ -59,20 +79,72 @@ def clean_password2(self): if password is not None and password != password2: raise forms.ValidationError("Las contraseñas no coinciden") return password2 + + def clean(self): + cleaned_data = super().clean() + province = cleaned_data.get('province') + municipality_id = self.data.get('municipality') + + if province and municipality_id: + try: + municipality = Municipality.objects.get(id=municipality_id, province=province) + cleaned_data['municipality'] = municipality + except Municipality.DoesNotExist: + self.add_error('municipality', _("The selected municipality does not belong to the selected province.")) + elif province: + self.add_error('municipality', _("You must select a municipality.")) + + return cleaned_data + def save(self, commit=True): user = super().save(commit=False) user.set_password(self.cleaned_data['password']) + user.gender = self.cleaned_data.get('gender') + user.age_range = self.cleaned_data.get('age_range') + user.municipality = self.cleaned_data.get('municipality') + if commit: user.save() return user class CustomUserChangeForm(UserChangeForm): + province = forms.ModelChoiceField( + queryset=Province.objects.all(), + label=_("Province"), + required=False, + ) + class Meta: model = CustomUser - fields = ['username', 'first_name', 'last_name', 'email', 'avatar', 'bio', 'location', 'type'] + fields = [ + 'username', 'first_name', 'last_name', 'email', 'avatar', 'bio', + 'type', 'gender', 'age_range', 'municipality', + ] help_texts = {field: '' for field in fields} + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + if self.instance and self.instance.municipality: + self.fields['province'].initial = self.instance.municipality.province + self.fields['province'].widget.attrs['data-current-province'] = self.instance.municipality.province_id + + def clean(self): + cleaned_data = super().clean() + province = cleaned_data.get('province') + municipality_id = self.data.get('municipality') + + if province and municipality_id: + try: + municipality = Municipality.objects.get(id=municipality_id, province=province) + cleaned_data['municipality'] = municipality + except Municipality.DoesNotExist: + self.add_error('municipality', _("The selected municipality does not belong to the selected province.")) + elif province: + self.add_error('municipality', _("You must select a municipality.")) + + return cleaned_data + def clean_avatar(self): """ Performs custom validation on the uploaded avatar file to strictly enforce diff --git a/web_app/migrations/0004_municipality_province_remove_customuser_location_and_more.py b/web_app/migrations/0004_municipality_province_remove_customuser_location_and_more.py new file mode 100644 index 0000000..52b41c2 --- /dev/null +++ b/web_app/migrations/0004_municipality_province_remove_customuser_location_and_more.py @@ -0,0 +1,64 @@ +# Generated by Django 6.0.3 on 2026-06-09 09:06 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('web_app', '0003_roleauditlog'), + ] + + operations = [ + migrations.CreateModel( + name='Municipality', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('ine_code', models.CharField(max_length=5, unique=True)), + ('name', models.CharField(max_length=255)), + ], + options={ + 'verbose_name': 'Municipio', + 'verbose_name_plural': 'Municipios', + 'ordering': ['name'], + }, + ), + migrations.CreateModel( + name='Province', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('ine_code', models.CharField(max_length=2, unique=True)), + ('name', models.CharField(max_length=255)), + ], + options={ + 'verbose_name': 'Provincia', + 'verbose_name_plural': 'Provincias', + 'ordering': ['name'], + }, + ), + migrations.RemoveField( + model_name='customuser', + name='location', + ), + migrations.AddField( + model_name='customuser', + name='age_range', + field=models.CharField(blank=True, choices=[('<18', 'Under 18'), ('18-30', '18-30'), ('31-50', '31-50'), ('>50', 'Over 50')], max_length=10, null=True), + ), + migrations.AddField( + model_name='customuser', + name='gender', + field=models.CharField(blank=True, choices=[('Male', 'Male'), ('Female', 'Female'), ('Non-binary', 'Non-binary'), ('Other', 'Other')], max_length=50, null=True), + ), + migrations.AddField( + model_name='customuser', + name='municipality', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='users', to='web_app.municipality'), + ), + migrations.AddField( + model_name='municipality', + name='province', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='municipalities', to='web_app.province'), + ), + ] diff --git a/web_app/models.py b/web_app/models.py index 2f3c49a..53374d5 100644 --- a/web_app/models.py +++ b/web_app/models.py @@ -9,6 +9,44 @@ class UserType(models.TextChoices): ADMIN = 'Admin', 'Admin' CONSUMER = 'Consumer', 'Consumer' + +class Gender(models.TextChoices): + MALE = 'Male', 'Male' + FEMALE = 'Female', 'Female' + NON_BINARY = 'Non-binary', 'Non-binary' + OTHER = 'Other', 'Other' + +class AgeRange(models.TextChoices): + UNDER_18 = '<18', 'Under 18' + BETWEEN_18_30 = '18-30', '18-30' + BETWEEN_31_50 = '31-50', '31-50' + OVER_50 = '>50', 'Over 50' + +class Province(models.Model): + ine_code = models.CharField(max_length=2, unique=True) + name = models.CharField(max_length=255) + + class Meta: + verbose_name = 'Provincia' + verbose_name_plural = 'Provincias' + ordering = ['name'] + def __str__(self): + return self.name + +class Municipality(models.Model): + ine_code = models.CharField(max_length=5, unique=True) + name = models.CharField(max_length=255) + province = models.ForeignKey(Province, on_delete=models.CASCADE, related_name='municipalities') + + class Meta: + verbose_name = 'Municipio' + verbose_name_plural = 'Municipios' + ordering = ['name'] + + def __str__(self): + return self.name + + class API(models.Model): port = models.IntegerField(unique=True) name = models.CharField(max_length=100, blank=True, default='') @@ -195,11 +233,29 @@ class CustomUser(AbstractUser): """ avatar = models.ImageField(upload_to='avatars/', null=True, blank=True) bio = models.TextField(blank=True, null=True) - location = models.CharField(max_length=255, blank=True, null=True) type = models.CharField( choices=UserType.choices, default=UserType.CONSUMER, ) + gender = models.CharField( + choices=Gender.choices, + max_length=50, + blank=True, + null=True + ) + age_range = models.CharField( + choices=AgeRange.choices, + max_length=10, + blank=True, + null=True + ) + municipality = models.ForeignKey( + Municipality, + on_delete=models.SET_NULL, + blank=True, + null=True, + related_name='users' + ) subscriptions = models.ManyToManyField('API', blank=True, related_name='subscribed_users') def __str__(self): diff --git a/web_app/urls.py b/web_app/urls.py index 193488c..2fe6d87 100644 --- a/web_app/urls.py +++ b/web_app/urls.py @@ -27,6 +27,8 @@ path('terms-use/', views.terms_use, name='terms_use'), path('privacy-policy/', views.privacy_policy, name='privacy_policy'), + path('api/municipalities//', views.municipalities_by_province, name='municipalities_api'), + #path('user/profile', views.profile, name='profile'), #path('user/history', views.history, name='history'), diff --git a/web_app/views.py b/web_app/views.py index fbf81bd..b598a1e 100644 --- a/web_app/views.py +++ b/web_app/views.py @@ -3,8 +3,14 @@ from django.utils.crypto import get_random_string from django.shortcuts import render, redirect, get_object_or_404 from web_app.forms import CustomUserAdminCreationForm, CustomUserCreationForm -from web_app.models import AgeRating, API, CustomUser, Director, Genre, Movie, UserProfile, Series, Contingut, \ - AgeRating, Valoracio, Visualitzacio +from web_app.models import ( + API, Director, Genre, AgeRating, Contingut, + Movie, Series, CustomUser, UserProfile, + Preferits, Valoracio, Visualitzacio, SyncLog, + RoleAuditLog, + Province, Municipality, +) + from web_app import utils import json from itertools import chain @@ -89,7 +95,7 @@ def register_view(request): user.subscriptions.set(platforms) login(request, user) return redirect('home') - return render(request, 'identify/register.html', {'form': form}) + return render(request, 'identify/register.html', {'form': form, 'provinces': Province.objects.all()}) @login_required(login_url='login') def redirect_by_role(request): @@ -251,7 +257,7 @@ def register_platform_click(request): # MAGIA DE DJANGO: Si existe, actualiza la fecha. Si no, lo crea. vis, created = Visualitzacio.objects.update_or_create( user=request.user, - contingut_id=contingut_id, + contingut_id=contingut_id, api_id=api_id, defaults={'data_visualitzacio': timezone.now()} ) @@ -265,3 +271,6 @@ def register_platform_click(request): return JsonResponse({'status': 'error', 'message': 'Método no permitido'}, status=405) +def municipalities_by_province(request, province_id): + municipalities = Municipality.objects.filter(province_id=province_id).values('id', 'name') + return JsonResponse(list(municipalities), safe=False) \ No newline at end of file From ed29e00f6a0c0bb7e1ce4c67b8593c2f2721ff90 Mon Sep 17 00:00:00 2001 From: Gerard Safont Date: Tue, 9 Jun 2026 11:56:08 +0200 Subject: [PATCH 04/11] feat: chec municipality exist --- users/views.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/users/views.py b/users/views.py index ff42e15..989750f 100644 --- a/users/views.py +++ b/users/views.py @@ -22,7 +22,7 @@ _ = gettext.gettext from web_app.forms import CustomUserChangeForm, CustomUserAdminCreationForm -from web_app.models import CustomUser, Movie, Series, UserProfile, API, Visualitzacio, UserType, RoleAuditLog +from web_app.models import CustomUser, Movie, Series, UserProfile, API, Visualitzacio, UserType, RoleAuditLog, Province, Municipality from .services import ( get_content_analytics, format_analytics_for_csv, @@ -81,7 +81,16 @@ def user_profile(request): else: form = CustomUserChangeForm(instance=request.user) - return render(request, 'users/profile/user_profile.html', {'form': form}) + current_province_id = None + if request.user.municipality: + current_province_id = request.user.municipality.province_id + + return render(request, 'users/profile/user_profile.html', { + 'form': form, + 'provinces': Province.objects.all(), + 'current_province_id': current_province_id, + 'current_municipality_id': request.user.municipality_id, + }) @login_required(login_url='login') @user_passes_test(is_consumer) From 295633955b38ebe8a470c4e384b9c461586e336e Mon Sep 17 00:00:00 2001 From: Gerard Safont Date: Tue, 9 Jun 2026 11:59:22 +0200 Subject: [PATCH 05/11] feat(ui-profile): modify and add the informations of register and profile --- Templates/identify/register.html | 70 +++++++++++++++++++ static/css/register.css | 35 ++++++++++ users/static/users/css/user_profile.css | 16 +++++ users/templates/users/base_profile.html | 2 +- .../templates/users/profile/user_profile.html | 64 ++++++++++++++++- users/templates/users/types/user_client.html | 28 +++++++- 6 files changed, 209 insertions(+), 6 deletions(-) diff --git a/Templates/identify/register.html b/Templates/identify/register.html index ebdca10..0dc0d2a 100644 --- a/Templates/identify/register.html +++ b/Templates/identify/register.html @@ -101,6 +101,51 @@

{% trans "Create Account" %}

{% endfor %} +
+ + {% for error in form.gender.errors %} + {{ error }} + {% endfor %} +
+ +
+ + {% for error in form.age_range.errors %} + {{ error }} + {% endfor %} +
+ +
+ + {% for error in form.province.errors %} + {{ error }} + {% endfor %} +
+ +
+ + {% for error in form.municipality.errors %} + {{ error }} + {% endfor %} +
+
diff --git a/users/templates/users/profile/user_profile.html b/users/templates/users/profile/user_profile.html index 2506275..93a8638 100644 --- a/users/templates/users/profile/user_profile.html +++ b/users/templates/users/profile/user_profile.html @@ -48,11 +48,71 @@
- - {{ form.location }} + + {{ form.gender }} +
+ +
+ + {{ form.age_range }} +
+ +
+ + +
+ +
+ +
+ + {% endblock %} \ No newline at end of file diff --git a/users/templates/users/types/user_client.html b/users/templates/users/types/user_client.html index e4e2aa9..11ddd18 100644 --- a/users/templates/users/types/user_client.html +++ b/users/templates/users/types/user_client.html @@ -28,13 +28,35 @@

{{ user.last_login|date:"d/m/Y" }}

+
+
{% trans "Gender" %}
+

+ {% if user.gender %} + {{ user.get_gender_display }} + {% else %} + {% trans "Not specified" %} + {% endif %} +

+
+ +
+
{% trans "Age range" %}
+

+ {% if user.age_range %} + {{ user.get_age_range_display }} + {% else %} + {% trans "Not specified" %} + {% endif %} +

+
+
{% trans "Location" %}

- {% if user.location %} - {{ user.location }} + {% if user.municipality %} + {{ user.municipality.name }} ({{ user.municipality.province.name }}) {% else %} - {% trans "Add your location" %} + {% trans "Not specified" %} {% endif %}

From 52c06d72b4931a46018be49e97a476b6959a84d3 Mon Sep 17 00:00:00 2001 From: Gerard Safont Date: Tue, 9 Jun 2026 17:27:06 +0200 Subject: [PATCH 06/11] feat(service): add the geters of gender age province --- users/services.py | 95 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 95 insertions(+) diff --git a/users/services.py b/users/services.py index c84f3df..f775c37 100644 --- a/users/services.py +++ b/users/services.py @@ -172,4 +172,99 @@ def get_director_top_list(start_date=None, end_date=None, platform_ids=None): return { 'labels': [d.name for d in data], 'values': [d.total_views for d in data] + } + +def _base_view_filters(start_date=None, end_date=None, platform_ids=None): + filters = Q() + if start_date and end_date: + filters &= Q(data_visualitzacio__range=(start_date, end_date)) + elif end_date: + filters &= Q(data_visualitzacio__lte=end_date) + if platform_ids: + filters &= Q(api_id__in=platform_ids) + return filters + +def get_views_by_gender(start_date=None, end_date=None, platform_ids=None): + filters = _base_view_filters(start_date, end_date, platform_ids) + data = ( + Visualitzacio.objects.filter(filters) + .exclude(user__gender__isnull=True) + .values('user__gender') + .annotate(total=Count('id')) + .order_by('-total') + ) + return { + 'labels': [d['user__gender'] for d in data], + 'values': [d['total'] for d in data], + } + +def get_views_by_age_range(start_date=None, end_date=None, platform_ids=None): + filters = _base_view_filters(start_date, end_date, platform_ids) + data = ( + Visualitzacio.objects.filter(filters) + .exclude(user__age_range__isnull=True) + .values('user__age_range') + .annotate(total=Count('id')) + .order_by('-total') + ) + return { + 'labels': [d['user__age_range'] for d in data], + 'values': [d['total'] for d in data], + } + +def get_views_by_province(start_date=None, end_date=None, platform_ids=None): + filters = _base_view_filters(start_date, end_date, platform_ids) + data = ( + Visualitzacio.objects.filter(filters) + .exclude(user__municipality__province__name__isnull=True) + .values('user__municipality__province__name') + .annotate(total=Count('id')) + .order_by('-total') + ) + labels = [d['user__municipality__province__name'] for d in data[:10]] + values = [d['total'] for d in data[:10]] + + if len(data) > 10: + others = sum(d['total'] for d in data[10:]) + labels.append('Others') + values.append(others) + + return {'labels': labels, 'values': values} + +def get_views_by_gender_age(start_date=None, end_date=None, platform_ids=None): + filters = _base_view_filters(start_date, end_date, platform_ids) + data = ( + Visualitzacio.objects.filter(filters) + .exclude(user__gender__isnull=True) + .exclude(user__age_range__isnull=True) + .values('user__gender', 'user__age_range') + .annotate(total=Count('id')) + .order_by('user__gender', 'user__age_range') + ) + + age_order = ['<18', '18-30', '31-50', '>50'] + gender_order = ['Male', 'Female', 'Non-binary', 'Other'] + + matrix = {} + for d in data: + g = d['user__gender'] + a = d['user__age_range'] + if g not in matrix: + matrix[g] = {} + matrix[g][a] = d['total'] + + datasets = [] + colors = ['#3498db', '#e74c3c', '#2ecc71', '#f39c12'] + for i, age in enumerate(age_order): + vals = [matrix.get(g, {}).get(age, 0) for g in gender_order if g in matrix] + datasets.append({ + 'label': age, + 'data': vals, + 'backgroundColor': colors[i % len(colors)], + }) + + present_genders = [g for g in gender_order if g in matrix] + return { + 'labels': present_genders, + 'datasets': datasets, } \ No newline at end of file From 7c681a0124d705ac155a4fac644cb6bdbc148d00 Mon Sep 17 00:00:00 2001 From: Gerard Safont Date: Tue, 9 Jun 2026 17:28:19 +0200 Subject: [PATCH 07/11] feat(views): add the views of demographics dashboard --- users/views.py | 115 ++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 113 insertions(+), 2 deletions(-) diff --git a/users/views.py b/users/views.py index 989750f..5fb0382 100644 --- a/users/views.py +++ b/users/views.py @@ -29,6 +29,10 @@ get_genre_distribution, get_age_rating_distribution, get_director_top_list, + get_views_by_gender, + get_views_by_age_range, + get_views_by_province, + get_views_by_gender_age, PII_COLUMNS, validate_gdpr_compliance, ) @@ -286,14 +290,17 @@ def crear_usuario_admin(request): def export_analytics_csv(request): start_date = request.GET.get('start') end_date = request.GET.get('end') - report_type = request.GET.get('type', 'internal') + report_type = request.GET.get('type', 'internal') + + if start_date == 'None': start_date = None + if end_date == 'None': end_date = None platform_ids = get_active_platform_ids(request.user) raw_data = get_content_analytics( start_date=start_date, end_date=end_date, - plataform_id=platform_ids # Ahora pasamos una lista de IDs segura + platform_ids=platform_ids ) is_b2b = (report_type == 'b2b') @@ -395,6 +402,110 @@ def admin_dashboard_directors(request): 'platforms': API.objects.all() }) +@login_required(login_url='login') +@user_passes_test(is_admin_or_staff) +def admin_dashboard_demographics(request): + start_date = request.GET.get('start') + end_date = request.GET.get('end') + platform_ids = get_active_platform_ids(request.user) + + gender_stats = get_views_by_gender(start_date, end_date, platform_ids) + age_stats = get_views_by_age_range(start_date, end_date, platform_ids) + province_stats = get_views_by_province(start_date, end_date, platform_ids) + cross_stats = get_views_by_gender_age(start_date, end_date, platform_ids) + + gender_table = list(zip(gender_stats['labels'], gender_stats['values'])) + age_table = list(zip(age_stats['labels'], age_stats['values'])) + province_table = list(zip(province_stats['labels'], province_stats['values'])) + + return render(request, 'users/parts/dashboard_demographics.html', { + 'gender_stats': gender_stats, + 'age_stats': age_stats, + 'province_stats': province_stats, + 'cross_stats': cross_stats, + 'gender_table': gender_table, + 'age_table': age_table, + 'province_table': province_table, + 'filters': {'start': start_date, 'end': end_date}, + 'platforms': API.objects.all(), + }) + +@login_required(login_url='login') +@user_passes_test(is_admin_or_staff) +def admin_dashboard_views_by_gender(request): + start_date = request.GET.get('start') + end_date = request.GET.get('end') + platform_ids = get_active_platform_ids(request.user) + + gender_stats = get_views_by_gender(start_date, end_date, platform_ids) + gender_table = list(zip(gender_stats['labels'], gender_stats['values'])) + + return render(request, 'users/parts/dashboard_views_by_gender.html', { + 'gender_stats': gender_stats, + 'gender_table': gender_table, + 'filters': {'start': start_date, 'end': end_date}, + 'platforms': API.objects.all(), + }) + +@login_required(login_url='login') +@user_passes_test(is_admin_or_staff) +def admin_dashboard_views_by_age(request): + start_date = request.GET.get('start') + end_date = request.GET.get('end') + platform_ids = get_active_platform_ids(request.user) + + age_stats = get_views_by_age_range(start_date, end_date, platform_ids) + age_table = list(zip(age_stats['labels'], age_stats['values'])) + + return render(request, 'users/parts/dashboard_views_by_age.html', { + 'age_stats': age_stats, + 'age_table': age_table, + 'filters': {'start': start_date, 'end': end_date}, + 'platforms': API.objects.all(), + }) + +@login_required(login_url='login') +@user_passes_test(is_admin_or_staff) +def admin_dashboard_views_by_province(request): + start_date = request.GET.get('start') + end_date = request.GET.get('end') + platform_ids = get_active_platform_ids(request.user) + + province_stats = get_views_by_province(start_date, end_date, platform_ids) + province_table = list(zip(province_stats['labels'], province_stats['values'])) + + return render(request, 'users/parts/dashboard_views_by_province.html', { + 'province_stats': province_stats, + 'province_table': province_table, + 'filters': {'start': start_date, 'end': end_date}, + 'platforms': API.objects.all(), + }) + +@login_required(login_url='login') +@user_passes_test(is_admin_or_staff) +def admin_dashboard_cross_tab(request): + start_date = request.GET.get('start') + end_date = request.GET.get('end') + platform_ids = get_active_platform_ids(request.user) + + cross_stats = get_views_by_gender_age(start_date, end_date, platform_ids) + + cross_table_headers = [d['label'] for d in cross_stats['datasets']] + cross_table_rows = [] + for i, label in enumerate(cross_stats['labels']): + row = [label] + for dataset in cross_stats['datasets']: + row.append(dataset['data'][i] if i < len(dataset['data']) else 0) + cross_table_rows.append(row) + + return render(request, 'users/parts/dashboard_cross_tab.html', { + 'cross_stats': cross_stats, + 'cross_table_headers': cross_table_headers, + 'cross_table_rows': cross_table_rows, + 'filters': {'start': start_date, 'end': end_date}, + 'platforms': API.objects.all(), + }) + @user_passes_test(lambda u: u.is_superuser) def update_user_role(request): if request.method == 'POST': From 96de2ce38d944b79a7ee54fb3811e1ebdbcac77e Mon Sep 17 00:00:00 2001 From: Gerard Safont Date: Tue, 9 Jun 2026 17:29:18 +0200 Subject: [PATCH 08/11] feat(urls): add the demographics dashboard urls and added in middleware --- users/middleware.py | 8 ++++++-- users/urls.py | 5 +++++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/users/middleware.py b/users/middleware.py index 7109573..c622a8a 100644 --- a/users/middleware.py +++ b/users/middleware.py @@ -41,8 +41,12 @@ def __call__(self, request): 'export_analytics_csv', 'logout', 'update_user_role', - ] - + 'admin_dashboard_demographics', + 'admin_dashboard_views_by_gender', + 'admin_dashboard_views_by_age', + 'admin_dashboard_views_by_province', + 'admin_dashboard_cross_tab', + ] if current_url_name not in exempt_url_names and not request.path.startswith('/static/'): return redirect(reverse('admin_dashboard')) return self.get_response(request) \ No newline at end of file diff --git a/users/urls.py b/users/urls.py index 6c7af8c..a06f191 100644 --- a/users/urls.py +++ b/users/urls.py @@ -23,4 +23,9 @@ path('admin/dashboard/genres/', views.admin_dashboard_genres, name='admin_dashboard_genres'), path('admin/dashboard/age-ratings/', views.admin_dashboard_age_ratings, name='admin_dashboard_age_ratings'), path('admin/dashboard/directors/', views.admin_dashboard_directors, name='admin_dashboard_directors'), + path('admin/dashboard/demographics/', views.admin_dashboard_demographics, name='admin_dashboard_demographics'), + path('admin/dashboard/demographics/gender/', views.admin_dashboard_views_by_gender, name='admin_dashboard_views_by_gender'), + path('admin/dashboard/demographics/age/', views.admin_dashboard_views_by_age, name='admin_dashboard_views_by_age'), + path('admin/dashboard/demographics/province/', views.admin_dashboard_views_by_province, name='admin_dashboard_views_by_province'), + path('admin/dashboard/demographics/cross-tab/', views.admin_dashboard_cross_tab, name='admin_dashboard_cross_tab'), ] \ No newline at end of file From 869acd90360dac87e57dda02d106b1787fecbe93 Mon Sep 17 00:00:00 2001 From: Gerard Safont Date: Tue, 9 Jun 2026 17:34:16 +0200 Subject: [PATCH 09/11] fix(export-csv): null evaluate error solved --- users/templates/users/parts/dashboard_overview.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/users/templates/users/parts/dashboard_overview.html b/users/templates/users/parts/dashboard_overview.html index 5148afd..237ce03 100644 --- a/users/templates/users/parts/dashboard_overview.html +++ b/users/templates/users/parts/dashboard_overview.html @@ -6,7 +6,7 @@
Views per Content
- Export CSV From eaf2d35d70e05905aca6bb63445175da1597137c Mon Sep 17 00:00:00 2001 From: Gerard Safont Date: Tue, 9 Jun 2026 17:35:52 +0200 Subject: [PATCH 10/11] feat(botton-dashboards): add teh botton of demographics dashboards --- users/templates/users/types/user_admin.html | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/users/templates/users/types/user_admin.html b/users/templates/users/types/user_admin.html index 5774e99..0463fd8 100644 --- a/users/templates/users/types/user_admin.html +++ b/users/templates/users/types/user_admin.html @@ -23,6 +23,10 @@ {% trans 'Directors' %} + From 170ceba4463629726415a1c528cc13a9d5b94d63 Mon Sep 17 00:00:00 2001 From: Gerard Safont Date: Tue, 9 Jun 2026 17:37:11 +0200 Subject: [PATCH 11/11] feat(demographics-dashboards): create the new demographics dashboards in admin porfile --- .../users/parts/dashboard_cross_tab.html | 97 ++++++++++++++ .../users/parts/dashboard_demographics.html | 122 ++++++++++++++++++ .../users/parts/dashboard_views_by_age.html | 81 ++++++++++++ .../parts/dashboard_views_by_gender.html | 81 ++++++++++++ .../parts/dashboard_views_by_province.html | 81 ++++++++++++ 5 files changed, 462 insertions(+) create mode 100644 users/templates/users/parts/dashboard_cross_tab.html create mode 100644 users/templates/users/parts/dashboard_demographics.html create mode 100644 users/templates/users/parts/dashboard_views_by_age.html create mode 100644 users/templates/users/parts/dashboard_views_by_gender.html create mode 100644 users/templates/users/parts/dashboard_views_by_province.html diff --git a/users/templates/users/parts/dashboard_cross_tab.html b/users/templates/users/parts/dashboard_cross_tab.html new file mode 100644 index 0000000..55ec2ef --- /dev/null +++ b/users/templates/users/parts/dashboard_cross_tab.html @@ -0,0 +1,97 @@ +{% extends 'users/types/user_admin.html' %} +{% load i18n %} + +{% block parts_content %} +
+ {% include 'users/parts/filter_form.html' %} + + + +
+
+
+
+
{% trans 'Views by Gender and Age Range' %}
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
{% trans 'Cross-tabulation Data' %}
+
+
+
+ + + + + {% for header in cross_table_headers %} + + {% endfor %} + + + + {% for row in cross_table_rows %} + + {% for cell in row %} + + {% endfor %} + + {% endfor %} + +
{% trans 'Gender' %}{{ header }}
{{ cell }}
+
+
+
+
+
+
+ + +{% endblock %} diff --git a/users/templates/users/parts/dashboard_demographics.html b/users/templates/users/parts/dashboard_demographics.html new file mode 100644 index 0000000..f6bca3c --- /dev/null +++ b/users/templates/users/parts/dashboard_demographics.html @@ -0,0 +1,122 @@ +{% extends 'users/types/user_admin.html' %} +{% load i18n %} + +{% block parts_content %} +
+ {% include 'users/parts/filter_form.html' %} + + + +
+
+
+
+
{% trans 'Views by Gender' %}
+
+
+
+ +
+
+
+
+ +
+
+
+
{% trans 'Views by Age Range' %}
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
{% trans 'Views by Province (Top 10)' %}
+
+
+
+ +
+
+
+
+ +
+
+
+
{% trans 'Gender x Age Range' %}
+
+
+
+ +
+
+
+
+
+
+ + +{% endblock %} diff --git a/users/templates/users/parts/dashboard_views_by_age.html b/users/templates/users/parts/dashboard_views_by_age.html new file mode 100644 index 0000000..2cda0d3 --- /dev/null +++ b/users/templates/users/parts/dashboard_views_by_age.html @@ -0,0 +1,81 @@ +{% extends 'users/types/user_admin.html' %} +{% load i18n %} + +{% block parts_content %} +
+ {% include 'users/parts/filter_form.html' %} + + + +
+
+
+
+
{% trans 'Views by Age Range' %}
+
+
+
+ +
+
+
+
+
+
+
+
{% trans 'Age Range Data' %}
+
+
+
+ + + + + + + + + {% for label, value in age_table %} + + + + + {% endfor %} + +
{% trans 'Age Range' %}{% trans 'Views' %}
{{ label }}{{ value }}
+
+
+
+
+
+
+ + +{% endblock %} diff --git a/users/templates/users/parts/dashboard_views_by_gender.html b/users/templates/users/parts/dashboard_views_by_gender.html new file mode 100644 index 0000000..b2a1a51 --- /dev/null +++ b/users/templates/users/parts/dashboard_views_by_gender.html @@ -0,0 +1,81 @@ +{% extends 'users/types/user_admin.html' %} +{% load i18n %} + +{% block parts_content %} +
+ {% include 'users/parts/filter_form.html' %} + + + +
+
+
+
+
{% trans 'Views by Gender' %}
+
+
+
+ +
+
+
+
+
+
+
+
{% trans 'Gender Data' %}
+
+
+
+ + + + + + + + + {% for label, value in gender_table %} + + + + + {% endfor %} + +
{% trans 'Gender' %}{% trans 'Views' %}
{{ label }}{{ value }}
+
+
+
+
+
+
+ + +{% endblock %} diff --git a/users/templates/users/parts/dashboard_views_by_province.html b/users/templates/users/parts/dashboard_views_by_province.html new file mode 100644 index 0000000..da78f0b --- /dev/null +++ b/users/templates/users/parts/dashboard_views_by_province.html @@ -0,0 +1,81 @@ +{% extends 'users/types/user_admin.html' %} +{% load i18n %} + +{% block parts_content %} +
+ {% include 'users/parts/filter_form.html' %} + + + +
+
+
+
+
{% trans 'Views by Province (Top 10)' %}
+
+
+
+ +
+
+
+
+
+
+
+
{% trans 'Province Data' %}
+
+
+
+ + + + + + + + + {% for label, value in province_table %} + + + + + {% endfor %} + +
{% trans 'Province' %}{% trans 'Views' %}
{{ label }}{{ value }}
+
+
+
+
+
+
+ + +{% endblock %}